@mega-yfue/eufy-sdk 0.5.0-beta.6 → 0.5.0-beta.60

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.
Files changed (55) hide show
  1. package/dist/client/device-registry.d.ts +24 -2
  2. package/dist/client/eufy-mega.d.ts +75 -10
  3. package/dist/client/types.d.ts +1 -1
  4. package/dist/core/contracts.d.ts +65 -30
  5. package/dist/core/store.d.ts +4 -0
  6. package/dist/core/types.d.ts +2 -0
  7. package/dist/index.js +3018 -425
  8. package/dist/index.js.map +4 -4
  9. package/dist/model/capabilities/arming.d.ts +37 -4
  10. package/dist/model/capabilities/camera.d.ts +24 -10
  11. package/dist/model/capabilities/dp-catalog.d.ts +4 -3
  12. package/dist/model/capabilities/index.d.ts +12 -3
  13. package/dist/model/capabilities/light.d.ts +3 -0
  14. package/dist/model/capabilities/motion.d.ts +5 -0
  15. package/dist/model/capabilities/mower.d.ts +230 -0
  16. package/dist/model/capabilities/suction.d.ts +5 -5
  17. package/dist/model/capabilities/types.d.ts +6 -0
  18. package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
  19. package/dist/model/classify.d.ts +1 -2
  20. package/dist/model/device-family.d.ts +4 -0
  21. package/dist/model/param-dictionary.d.ts +6 -0
  22. package/dist/model/param-namespace.d.ts +1 -1
  23. package/dist/model/registry.d.ts +3 -2
  24. package/dist/model/types.d.ts +21 -8
  25. package/dist/transport/abortable.d.ts +8 -0
  26. package/dist/transport/http/mega-client.d.ts +34 -2
  27. package/dist/transport/mqtt/broker-discovery.d.ts +2 -1
  28. package/dist/transport/mqtt/command-router.d.ts +3 -2
  29. package/dist/transport/p2p/command-router.d.ts +14 -26
  30. package/dist/transport/p2p/fmp4.d.ts +12 -2
  31. package/dist/transport/p2p/live-stream.d.ts +3 -0
  32. package/dist/transport/p2p/p2p-session.d.ts +21 -1
  33. package/dist/transport/p2p/readable-egress.d.ts +4 -0
  34. package/dist/transport/p2p/recording-download.d.ts +4 -3
  35. package/dist/transport/p2p/shared-live-source.d.ts +9 -0
  36. package/dist/transport/p2p/talkback.d.ts +21 -10
  37. package/dist/transport/p2p/video.d.ts +5 -2
  38. package/dist/transport/rtc/command-router.d.ts +128 -0
  39. package/dist/transport/rtc/live.d.ts +69 -0
  40. package/dist/transport/rtc/peer.d.ts +104 -0
  41. package/dist/transport/rtc/portal-keepalive.d.ts +11 -0
  42. package/dist/transport/rtc/portal-packet.d.ts +72 -0
  43. package/dist/transport/rtc/ptcs-framer.d.ts +86 -0
  44. package/dist/transport/rtc/scall-sdp.d.ts +42 -0
  45. package/dist/transport/rtc/session.d.ts +90 -0
  46. package/dist/transport/rtc/signaling.d.ts +139 -0
  47. package/dist/transport/token-echo.d.ts +8 -0
  48. package/dist/transport/tuya/account.d.ts +8 -5
  49. package/dist/transport/tuya/client.d.ts +19 -5
  50. package/dist/transport/tuya/command-router.d.ts +10 -14
  51. package/dist/transport/tuya/dp-codec.d.ts +2 -23
  52. package/dist/transport/tuya/et3.d.ts +22 -0
  53. package/dist/transport/tuya/request.d.ts +18 -8
  54. package/dist/transport/tuya/sign.d.ts +7 -0
  55. package/package.json +2 -1
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
39
39
  * it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
40
40
  * new capabilities are modelled — adding one never requires a subclass.
41
41
  */
42
- export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "display" | "info";
42
+ export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "mower" | "smart_light" | "display" | "info";
43
43
  /** Value type of a property. */
44
44
  export type PropertyValueType = "bool" | "number" | "string" | "enum";
45
45
  /**
@@ -52,11 +52,12 @@ export type PropertyValueType = "bool" | "number" | "string" | "enum";
52
52
  * one as the other is wrong in a way no type check catches.
53
53
  *
54
54
  * - `boolean` — an on/off state (always paired with `type: "bool"`).
55
- * - `percent` / `celsius` / `dbm` / `seconds` / `hours` / `megabytes` / `degrees` — a measured quantity
56
- * in the unit the device reports it in; each pairs with the matching `unit`. Values are
57
- * never converted on the way out — a converted reading is an invented one. `seconds` and `hours`
58
- * are separate kinds for exactly that reason: a robot reports a run in seconds and a consumable's
59
- * wear in hours, and normalising one into the other would publish a number the device never sent.
55
+ * - `percent` / `celsius` / `dbm` / `seconds` / `hours` / `minutes` / `millimetres` / `megabytes` /
56
+ * `degrees` — a measured quantity in the unit the device reports it in; each pairs with the
57
+ * matching `unit`. Values are never converted on the way out — a converted reading is an invented
58
+ * one. `seconds`, `minutes` and `hours` are separate kinds for exactly that reason: a robot reports
59
+ * a run in seconds, a mower its blade wear in minutes and a vacuum's consumable wear in hours, and
60
+ * normalising one into another would publish a number the device never sent.
60
61
  * - `scalar` — a plain number in no unit at all: a step on a ladder, a mode index, a raw level, a
61
62
  * segment count. Ordered and comparable, but its range and direction are the device's, so nothing
62
63
  * but the device says what a given value means.
@@ -83,7 +84,7 @@ export type PropertyValueType = "bool" | "number" | "string" | "enum";
83
84
  * decodes them and are exported beside that decoder; unlike an enum's options they are not carried
84
85
  * here, which is a gap this vocabulary does not close on its own.
85
86
  */
86
- export declare const KNOWN_VALUE_KINDS: readonly ["boolean", "percent", "celsius", "dbm", "seconds", "hours", "megabytes", "degrees", "scalar", "bitfield", "enum", "identifier", "timestamp", "text"];
87
+ export declare const KNOWN_VALUE_KINDS: readonly ["boolean", "percent", "celsius", "dbm", "seconds", "hours", "minutes", "millimetres", "megabytes", "degrees", "scalar", "bitfield", "enum", "identifier", "timestamp", "text"];
87
88
  /** One of the kinds this version models — the closed half of {@link ValueKind}. */
88
89
  export type KnownValueKind = (typeof KNOWN_VALUE_KINDS)[number];
89
90
  /**
@@ -106,7 +107,8 @@ export declare function isKnownValueKind(kind: ValueKind): kind is KnownValueKin
106
107
  /**
107
108
  * How much a param's name and meaning can be trusted, most-trusted first. This is the one definition;
108
109
  * the param dictionary and every `provenance` field use it.
109
- * - `mega` — the vendor's cloud names it: its data-point catalog (`get_product_data_point`), or a
110
+ * - `mega` — the vendor's cloud names it: its data-point catalog (`get_product_data_point`), the
111
+ * Tuya product schema of an `eufy_home_tuya` device (`thing.m.device.ref.info.list`), or a
110
112
  * reported value that matches what the cloud record already says (a model name or code).
111
113
  * - `verified` — confirmed on a real device by this project: our own capture or observation, or
112
114
  * identified by someone who has the hardware.
@@ -140,6 +142,11 @@ export interface PropertySpec {
140
142
  kind?: ValueKind;
141
143
  /** Whether the value can be written back to the device (a setter exists). */
142
144
  writable: boolean;
145
+ /**
146
+ * Whether the device has reported this value: its own param, a read alias, or the payload it reads
147
+ * from. `false` means it never has. Absent when the manifest was built without the device's params.
148
+ */
149
+ reported?: boolean;
143
150
  /** Allowed values for `type: "enum"` (raw → label). */
144
151
  enumValues?: Record<number, string>;
145
152
  /**
@@ -286,6 +293,12 @@ export interface CloudRecord {
286
293
  parentSn?: string;
287
294
  /** Reported param_type → raw value. Presence of a param is a capability signal. */
288
295
  params?: Record<number, string>;
296
+ /**
297
+ * Params the device reported over its realtime channel, keyed in the device's own namespace. A line
298
+ * whose state exists only on that channel carries none of it in {@link params}, so this is the
299
+ * evidence an availability gate on such a param is decided from.
300
+ */
301
+ dpParams?: Record<number, string>;
289
302
  }
290
303
  /** The fully-resolved device shape produced by the 3-tier resolver. */
291
304
  export interface ResolvedDevice {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Settle `work` as it settles, or reject the moment `signal` aborts, whichever comes first.
3
+ *
4
+ * The underlying wait is left to finish on its own: it may be shared work other callers are waiting on, so a
5
+ * caller abandoning its own call does not cancel the work itself. This abandons WAITING, which is the only
6
+ * part that belonged to the caller.
7
+ */
8
+ export declare function abortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -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. No credentials means nothing to try,
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, not the 0x80 deny code). */
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 — such a device reports as offline
140
- * (the throw below) until the backend grants the account's own credentials.
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
@@ -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";
@@ -258,11 +247,14 @@ export declare class P2PCommandRouter {
258
247
  */
259
248
  dispatchCommand(sn: string, cmd: Command): Promise<void>;
260
249
  /**
261
- * Restart a HomeBase. `RESTART_HUB` (1034) is a station-scalar on the broadcast channel 255: a
262
- * level-2 frame whose body is `[u32 value][account_id padded]` — the same shape as the hub
263
- * alarm-volume control. ✅ Wire-confirmed byte-exact from a capture of the app's own Restart
264
- * (2026-08-03) and HW-tested: the captured frame carried value `0` and rebooted the hub. Replays
265
- * like every other level-2 control, so a single dropped datagram doesn't lose it.
250
+ * Restart the station that owns `sn`'s P2P session. `RESTART_HUB` (1034) is a station-scalar on the
251
+ * broadcast channel 255 whose body is `[u32 value][account_id padded]`, value `0`. ✅ Wire-confirmed
252
+ * byte-exact as a level-2 frame from a capture of the app's own HomeBase Restart (2026-08-03).
253
+ *
254
+ * The seal follows the session, as {@link sendBySessionLevel} defines it: a keyed session takes the
255
+ * level-2 frame, a keyless one — a standalone camera, which never negotiates a key — the same body
256
+ * sealed level-1. Both are replayed {@link DIRECT_CMD_SENDS}× at 200ms, as every other unacknowledged
257
+ * control here is.
266
258
  */
267
259
  rebootStation(sn: string): Promise<void>;
268
260
  /**
@@ -308,6 +300,11 @@ export declare class P2PCommandRouter {
308
300
  * stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
309
301
  * instead of playing into silence.
310
302
  *
303
+ * **On a model in {@link ELD_TALKBACK_MODELS}, the speaker plays the codec the camera sends.** So the first
304
+ * audio frame of the media session this talkback holds decides its codec ({@link speakerCodec}), within
305
+ * {@link TALKBACK_CODEC_WAIT_MS} of the session's first frame, and the claim is re-checked after that wait as
306
+ * after the warm-up. Every other model opens `aac-lc` at once.
307
+ *
311
308
  * The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
312
309
  * {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
313
310
  * own-session camera that legitimately never negotiates one.
@@ -453,15 +450,6 @@ export declare class P2PCommandRouter {
453
450
  * A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
454
451
  * unaffected.
455
452
  */
456
- /**
457
- * Attach a consumer, unless the caller has already abandoned the call.
458
- *
459
- * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
460
- * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
461
- * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
462
- * not.
463
- */
464
- private attachUnlessAborted;
465
453
  /**
466
454
  * Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
467
455
  * on a session goes with it, and a media session opened for this source alone is closed: nothing else
@@ -687,7 +675,7 @@ export declare class P2PCommandRouter {
687
675
  * **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
688
676
  * exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
689
677
  * capture of the app's own SET_SNOOZE_TIME (1271) frame — see `param-dictionary.ts`'s `1271` entry
690
- * (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.ALARM_DELAY_CONFIG`) reuses
678
+ * (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.SET_ALL_ACTION`) reuses
691
679
  * the same bare-JSON shape.
692
680
  */
693
681
  private sendJsonRaw;
@@ -17,9 +17,12 @@ export declare class Fmp4Muxer {
17
17
  private baseDecodeTime;
18
18
  private fragTicks;
19
19
  private samples;
20
- private lastVideoTimestampMs?;
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
- /** Add one station-declared AAC access unit to the audio track. G.711 remains available via `live()`. */
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;
@@ -103,6 +103,9 @@ export declare class LiveStream extends EventEmitter {
103
103
  private lastCodec;
104
104
  /** Rebuilds an access unit the station split across several frames — see {@link AccessUnitAssembler}. */
105
105
  private readonly units;
106
+ /** Whether a missing video datagram or incomplete unit invalidated the current reference chain. */
107
+ private awaitingKeyframeAfterGap;
108
+ private readonly videoGapHandler;
106
109
  /** The channel inbound media must be tagged with, once {@link acceptsMedia} trusts the station's tag. */
107
110
  private mediaChannel?;
108
111
  private tracedFirstVideoCommand;
@@ -435,6 +435,11 @@ export declare class P2PSession extends EventEmitter {
435
435
  * "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
436
436
  */
437
437
  sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
438
+ /**
439
+ * The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
440
+ * level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
441
+ */
442
+ sendRawLevel1Bytes(payload: Buffer, channel: number, outerCmd: number): boolean;
438
443
  /**
439
444
  * Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
440
445
  * its runtime topology takes, as {@link startLiveMedia} selects its start:
@@ -606,6 +611,18 @@ export declare class P2PSession extends EventEmitter {
606
611
  accountId?: string;
607
612
  channel?: number;
608
613
  }): void;
614
+ /**
615
+ * Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
616
+ * recording rows with companion tables; each group retains its `table_name` and `payload`.
617
+ * `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
618
+ */
619
+ queryRecordPage(opts: {
620
+ accountId?: string;
621
+ startDate: string;
622
+ endDate: string;
623
+ startTime?: string;
624
+ count?: number;
625
+ }): Promise<unknown[]>;
609
626
  /**
610
627
  * Query one on-station table and answer its rows, once the reply is whole.
611
628
  *
@@ -613,13 +630,15 @@ export declare class P2PSession extends EventEmitter {
613
630
  * several frames whose decrypted text is a fragment of one document, so the fragments are
614
631
  * accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
615
632
  * `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
616
- * complete reply.
633
+ * complete reply, and when the station answers a non-zero `mIntRet`.
617
634
  *
618
635
  * One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
619
636
  * session throws, so a second reply cannot land in this buffer.
620
637
  */
621
638
  readDatabase(table: string, opts?: {
622
639
  accountId?: string;
640
+ innerCmd?: number;
641
+ query?: Record<string, unknown>;
623
642
  timeoutMs?: number;
624
643
  signal?: AbortSignal;
625
644
  }): Promise<unknown[]>;
@@ -699,6 +718,7 @@ export declare class P2PSession extends EventEmitter {
699
718
  * Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
700
719
  * of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
701
720
  * it would lose that frame and every frame after it until a datagram happened to begin on a header.
721
+ * Payload fragments retain their original slices and are copied once, when the complete frame is delivered.
702
722
  */
703
723
  private reassemble;
704
724
  /**
@@ -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. Keyframes go through one
50
- * {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not open under
51
- * `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
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;
@@ -438,3 +438,12 @@ export declare class SharedLiveSource {
438
438
  */
439
439
  dispose(): void;
440
440
  }
441
+ /**
442
+ * Attach a consumer, unless the caller has already abandoned the call.
443
+ *
444
+ * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
445
+ * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
446
+ * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
447
+ * not.
448
+ */
449
+ 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 AAC-LC audio from
3
- * a host, recovers frame boundaries, and paces the frames onto the wire at their own playback rate,
4
- * bracketed by the device's start/stop control frames.
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 the device plays what arrives when it arrives.
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 fixed 64 ms tick until {@link stop}.
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
- private readonly highWater;
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. Bytes are ADTS AAC, or PCM when an encoder was supplied; either way the input is
107
- * treated as a stream, so a chunk carrying part of a frame is held until the rest arrives.
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. */
@@ -144,9 +146,10 @@ export declare class AccessUnitAssembler {
144
146
  * `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
145
147
  * frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
146
148
  * of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
147
- * carries).
149
+ * carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
150
+ * as a complete unit, even if its repeated header says keyframe.
148
151
  */
149
152
  push(payload: Buffer, decode: (payload: Buffer) => Buffer | undefined): AssembledAccessUnit[];
150
153
  /** Forget an incomplete unit, counting and reporting it. */
151
- private discard;
154
+ discard(): void;
152
155
  }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * RTC command router: drives a station over the portal's WebRTC data channel instead of P2P.
3
+ *
4
+ * The T9000 answers no P2P lookup, but it accepts the portal's control channel: sign → WS join → scall →
5
+ * SDP answer → ICE over the relay the hub grants → DTLS → SCTP data channels, with commands riding
6
+ * `WebrtcDataChannel` as portal packets (see {@link buildPortalPacket}).
7
+ *
8
+ * `set-payload` rides the `1350` SET_PAYLOAD envelope, inner `{account_id, cmd, mValue3, payload}`;
9
+ * `set-json` rides the `1700` CONTROL_PAYLOAD envelope, inner `{account_id, cmd, commandType, data}`.
10
+ * A command to the station goes on the station channel `255`; an attached device's command shares the
11
+ * station's session and keeps the device's channel. Other command kinds are refused.
12
+ *
13
+ * One session per station, reused across commands and closed after {@link IDLE_CLOSE_MS} without one;
14
+ * a live pull holds it open. Sends on a session are serialised; an ACK is correlated by its outer command
15
+ * and segment, since it carries no inner command. An ACK is not an observation of the resulting state.
16
+ *
17
+ * Live view of an attached camera rides the same session (the hub accepts one RTC session per account),
18
+ * through one shared live source per station: the station serves one camera at a time, so a second
19
+ * camera is refused until the first one's view ends.
20
+ */
21
+ import { type Command, type MediaProvider } from "../../core/contracts.js";
22
+ import type { Logger } from "../../core/logger.js";
23
+ import { RtcSession, type RtcSessionOptions } from "./session.js";
24
+ export interface RtcIdentity {
25
+ authToken: string;
26
+ userId: string;
27
+ /** The `gtoken` header value the mega session's authed HTTP calls carry. */
28
+ gtoken: string;
29
+ }
30
+ /** Where a command goes: the station's session, and whether it addresses the station or a device on it. */
31
+ export interface RtcRoute {
32
+ /** The station's signaling exchange; defaults to compact SDP. */
33
+ signalingMode?: "call" | "scall";
34
+ /** Candidate policy used by the station's exchange; defaults to relay-only. */
35
+ iceTransportPolicy?: "relay" | "all";
36
+ stationSn: string;
37
+ /**
38
+ * The station's `member.admin_user_id`, the account the session and the payload name; the logged-in
39
+ * user when the station's record carries none.
40
+ */
41
+ adminUserId?: string;
42
+ /** True for a device attached to the station, which keeps the command's own channel. */
43
+ attached: boolean;
44
+ }
45
+ /** The camera a live view is for, on its station's route. */
46
+ export interface RtcLiveRoute extends RtcRoute {
47
+ cameraSn: string;
48
+ /** The camera's `device_channel` on the station. */
49
+ channel: number;
50
+ }
51
+ export interface RtcCommandRouterDeps {
52
+ /** The logged-in session's credentials; `undefined` while logged out. */
53
+ identity: () => RtcIdentity | undefined;
54
+ /** The mega shard the account signs on (`"ie-pr"`, `"eu-pr"`, `"us-pr"`); picks the smart host. */
55
+ shard: () => string;
56
+ /** ISO country sent on the sign request (default `US`). */
57
+ country?: string;
58
+ logger?: Logger;
59
+ onError?: (e: Error) => void;
60
+ /** ffmpeg for the live still (`ffmpeg` on PATH by default). */
61
+ ffmpegPath?: string;
62
+ createSession?: (opts: RtcSessionOptions) => RtcSession;
63
+ }
64
+ export declare class RtcCommandRouter {
65
+ private readonly deps;
66
+ private readonly sessions;
67
+ private readonly lives;
68
+ private readonly warm;
69
+ constructor(deps: RtcCommandRouterDeps);
70
+ dispatchCommand(route: RtcRoute, cmd: Command): Promise<void>;
71
+ /**
72
+ * The media of a camera on a T9000: live video, a readable feed and a live still over the station's
73
+ * session. Recording has no wire here and is refused; the optional members it has no wire for are absent.
74
+ */
75
+ mediaProviderFor(route: RtcLiveRoute): MediaProvider;
76
+ /**
77
+ * Tear down every station session (logout / shutdown). Each live source is disposed before its session
78
+ * closes, so its consumers end cleanly and its stop still rides the session.
79
+ */
80
+ close(): void;
81
+ /**
82
+ * Hand `use` the station's live source for this camera, in the same turn the source is created or reused,
83
+ * so a consumer it attaches is counted before any other caller looks.
84
+ *
85
+ * The station streams one camera at a time. Another camera whose source has consumers refuses this one,
86
+ * before the session is awaited and again after; one with none (never attached or lingering)
87
+ * holds nothing a viewer needs, so it is disposed and the slot taken. The first caller's
88
+ * {@link SharedSourceHints} configure a new source.
89
+ */
90
+ private withLiveSource;
91
+ /** Refuse a camera while another camera on its station has consumers. */
92
+ private refuseIfWatched;
93
+ private newLiveSource;
94
+ /**
95
+ * Completes on the command ACK or a correlated control-result notification. The notification must
96
+ * match the request channel, parameter and segment and contain a structured payload. Receipt does
97
+ * not establish physical actuation. The command is sent once, bounded by {@link ACK_TIMEOUT_MS}.
98
+ */
99
+ private sendAwaitAck;
100
+ private stationSession;
101
+ /**
102
+ * One bounded bring-up: the session is up when its command channel opens, and it fails, with the
103
+ * session closed and every listener gone, when `connect()` throws, the session closes first, or
104
+ * {@link CONNECT_TIMEOUT_MS} passes. A single promise, so an early failure cannot leave a second one
105
+ * rejecting unheard.
106
+ */
107
+ private bringUp;
108
+ /**
109
+ * Hold a station's command session open: one hold per station, its idle-close ({@link IDLE_CLOSE_MS})
110
+ * suppressed, {@link PORTAL_KEEPALIVE} sent every {@link PORTAL_KEEPALIVE_MS}, and the session dropped
111
+ * on a tick that finds the previous keepalive unechoed. A closed session ends the hold.
112
+ *
113
+ * Answers an idempotent disposer that releases the hold. Throws when no account is logged in.
114
+ */
115
+ keepWarm(route: RtcRoute): () => void;
116
+ /**
117
+ * Bring a station session up for a {@link keepWarm} hold and start its keepalive. A bring-up that fails,
118
+ * or a hold disposed before it completes, clears the hold and arms no timer. Each tick first checks
119
+ * that the hub echoed the previous keepalive, and drops the session when it did not. The keepalive
120
+ * stops when the session closes, which also clears the hold so the next command or `keepWarm`
121
+ * rebuilds cleanly.
122
+ */
123
+ private warmUp;
124
+ /** Stop a hold's keepalive and remove its listeners from the session it watches. */
125
+ private unwatch;
126
+ private touch;
127
+ private drop;
128
+ }