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

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 +3034 -432
  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 +16 -28
  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 +27 -13
  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
@@ -521,11 +509,11 @@ export declare class P2PCommandRouter {
521
509
  */
522
510
  private onLiveStartFailed;
523
511
  /**
524
- * Replace the session under a warming source whose media start nothing acknowledged, and warm again on it.
512
+ * Replace the session under a warming source it is not serving, and warm again on it.
525
513
  *
526
514
  * Only a STANDALONE device's session is replaced, for the reason {@link onLiveStartFailed} gives: an attached
527
515
  * camera shares its HomeBase session with every other camera on it, and closing that to recover one would
528
- * drop the rest. Such a source keeps the re-issue it always had.
516
+ * drop the rest. Such a source keeps the re-issue it always had, which the answer (false) tells it.
529
517
  *
530
518
  * The source is left warming throughout, holding the deadline it started, so this either produces a stream
531
519
  * within that window or fails exactly as it would have. A replacement that cannot be opened leaves the
@@ -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;
@@ -83,12 +83,13 @@ export interface SharedLiveSourceOptions {
83
83
  */
84
84
  onStopped?: () => void;
85
85
  /**
86
- * A media start was abandoned unacknowledged before anything was delivered, so this session is not being
87
- * heard. The owner is asked for a replacement and calls {@link SharedLiveSource.rewarm} once it has one.
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 abandonments are the same session saying the same thing.
90
+ * Asked at most once per warm-up: further evidence is the same session saying the same thing.
90
91
  */
91
- onSessionUnreachable?: () => void;
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
- * A start was abandoned unacknowledged. Ask for a replacement session where nothing has been delivered yet.
255
+ * Ask the owner for a replacement session where nothing has been delivered yet, answering whether one is coming.
255
256
  *
256
- * The abandonment is roughly twenty byte-identical sends with no reply, against acknowledgement latencies of
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
- * `P2PSession` says as much: the camera was never told to stream, so this warm-up can only time out. Where
259
- * media has already flowed the abandonment means something else and this does nothing.
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 while a replacement is awaited, because every tick it issues goes to the same
262
- * unheard session. The DEADLINE is left running: the window belongs to the attempt, not to the session it
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 onStartUnacknowledged;
265
+ private askForFreshSession;
266
266
  /**
267
267
  * Warm again on a session the owner has replaced, inside the deadline the first attempt started.
268
268
  *
@@ -290,6 +290,11 @@ export declare class SharedLiveSource {
290
290
  * {@link fruitlessReissues} for why one is not enough and why what the stream delivered earlier is not
291
291
  * evidence. Once media arrives the keepalive is what is wanted, and an attached camera re-sends a full start
292
292
  * either way.
293
+ *
294
+ * A forced start that a whole interval later has still produced nothing is a session that acknowledges and
295
+ * does not serve: a fresh session on the same camera delivers a keyframe within a second, so a warm-up asks
296
+ * for one rather than forcing starts onto this one until its deadline. Wired only: a cold battery camera is
297
+ * slow rather than deaf, measured at about 8 s to first media, and replacing its session would cost a wake.
293
298
  */
294
299
  private reissueStart;
295
300
  /**
@@ -438,3 +443,12 @@ export declare class SharedLiveSource {
438
443
  */
439
444
  dispose(): void;
440
445
  }
446
+ /**
447
+ * Attach a consumer, unless the caller has already abandoned the call.
448
+ *
449
+ * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
450
+ * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
451
+ * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
452
+ * not.
453
+ */
454
+ export declare function attachUnlessAborted(source: SharedLiveSource, signal?: AbortSignal): Consumer;
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Managed talkback session over P2P — the send-side twin of `live-stream.ts`. Takes 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
  }