@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.0

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 (143) hide show
  1. package/README.md +31 -12
  2. package/dist/client/device-registry.d.ts +343 -0
  3. package/dist/client/eufy-mega.d.ts +872 -0
  4. package/dist/client/index.d.ts +1 -6
  5. package/dist/client/map-channels.d.ts +22 -0
  6. package/dist/client/types.d.ts +383 -0
  7. package/dist/core/contracts.d.ts +898 -0
  8. package/dist/core/crypto.d.ts +98 -0
  9. package/dist/core/index.d.ts +9 -7
  10. package/dist/core/logger.d.ts +53 -0
  11. package/dist/core/lz4-block.d.ts +35 -0
  12. package/dist/core/raw-dp-hex.d.ts +32 -0
  13. package/dist/core/raw-dp-writer.d.ts +83 -0
  14. package/dist/core/store.d.ts +43 -0
  15. package/dist/core/types.d.ts +169 -0
  16. package/dist/core/util.d.ts +78 -0
  17. package/dist/index.d.ts +4 -7
  18. package/dist/index.js +25526 -14
  19. package/dist/index.js.map +7 -1
  20. package/dist/model/capabilities/access.d.ts +127 -0
  21. package/dist/model/capabilities/arming.d.ts +201 -0
  22. package/dist/model/capabilities/audio.d.ts +154 -0
  23. package/dist/model/capabilities/battery.d.ts +363 -0
  24. package/dist/model/capabilities/camera.d.ts +564 -0
  25. package/dist/model/capabilities/co.d.ts +40 -0
  26. package/dist/model/capabilities/contact.d.ts +133 -0
  27. package/dist/model/capabilities/doorbell.d.ts +345 -0
  28. package/dist/model/capabilities/dp-catalog.d.ts +38 -0
  29. package/dist/model/capabilities/index.d.ts +561 -0
  30. package/dist/model/capabilities/info.d.ts +28 -0
  31. package/dist/model/capabilities/keypad.d.ts +61 -0
  32. package/dist/model/capabilities/leak.d.ts +43 -0
  33. package/dist/model/capabilities/light.d.ts +174 -0
  34. package/dist/model/capabilities/locate.d.ts +63 -0
  35. package/dist/model/capabilities/lock.d.ts +242 -0
  36. package/dist/model/capabilities/manifest.d.ts +107 -0
  37. package/dist/model/capabilities/members.d.ts +647 -0
  38. package/dist/model/capabilities/motion.d.ts +377 -0
  39. package/dist/model/capabilities/person-detection.d.ts +8 -0
  40. package/dist/model/capabilities/ptz.d.ts +289 -0
  41. package/dist/model/capabilities/rtsp.d.ts +221 -0
  42. package/dist/model/capabilities/siren.d.ts +218 -0
  43. package/dist/model/capabilities/smart-light.d.ts +172 -0
  44. package/dist/model/capabilities/smoke.d.ts +40 -0
  45. package/dist/model/capabilities/snapshot.d.ts +6 -0
  46. package/dist/model/capabilities/storage.d.ts +11 -0
  47. package/dist/model/capabilities/suction.d.ts +104 -0
  48. package/dist/model/capabilities/types.d.ts +484 -0
  49. package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
  50. package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
  51. package/dist/model/capabilities/video.d.ts +6 -0
  52. package/dist/model/classify.d.ts +78 -0
  53. package/dist/model/clean-record-detail.d.ts +65 -0
  54. package/dist/model/clean-records.d.ts +69 -0
  55. package/dist/model/device-family.d.ts +73 -0
  56. package/dist/model/device-types.d.ts +123 -0
  57. package/dist/model/device.d.ts +265 -0
  58. package/dist/model/index.d.ts +29 -4
  59. package/dist/model/infer.d.ts +23 -0
  60. package/dist/model/inspect.d.ts +61 -0
  61. package/dist/model/life-params.d.ts +21 -0
  62. package/dist/model/map-pixels.d.ts +70 -0
  63. package/dist/model/param-dictionary.d.ts +28 -0
  64. package/dist/model/param-namespace.d.ts +21 -0
  65. package/dist/model/proto-read.d.ts +53 -0
  66. package/dist/model/push-events.d.ts +147 -0
  67. package/dist/model/registry.d.ts +54 -0
  68. package/dist/model/types.d.ts +301 -0
  69. package/dist/model/vacuum-map-store.d.ts +92 -0
  70. package/dist/model/vacuum-map.d.ts +286 -0
  71. package/dist/model/vacuum-scenes.d.ts +76 -0
  72. package/dist/model/vacuum-schedules.d.ts +85 -0
  73. package/dist/transport/dp-preset.d.ts +102 -0
  74. package/dist/transport/ff09.d.ts +444 -0
  75. package/dist/transport/ffmpeg.d.ts +86 -0
  76. package/dist/transport/http/decodeImageV1.d.ts +20 -0
  77. package/dist/transport/http/decodeImageV2.d.ts +19 -0
  78. package/dist/transport/http/index.d.ts +5 -0
  79. package/dist/transport/http/light-catalog.d.ts +62 -0
  80. package/dist/transport/http/media-download.d.ts +14 -0
  81. package/dist/transport/http/mega-client.d.ts +514 -0
  82. package/dist/transport/http/phone-model.d.ts +21 -0
  83. package/dist/transport/index.d.ts +10 -7
  84. package/dist/transport/mqtt/app-client-id.d.ts +16 -0
  85. package/dist/transport/mqtt/availability.d.ts +14 -0
  86. package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
  87. package/dist/transport/mqtt/biz-stream.d.ts +98 -0
  88. package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
  89. package/dist/transport/mqtt/clean-codec.d.ts +14 -0
  90. package/dist/transport/mqtt/command-router.d.ts +285 -0
  91. package/dist/transport/mqtt/dp-codec.d.ts +58 -0
  92. package/dist/transport/mqtt/dp-color.d.ts +14 -0
  93. package/dist/transport/mqtt/engine.d.ts +16 -0
  94. package/dist/transport/mqtt/index.d.ts +5 -0
  95. package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
  96. package/dist/transport/mqtt/topics.d.ts +80 -0
  97. package/dist/transport/p2p/adts.d.ts +91 -0
  98. package/dist/transport/p2p/annexb.d.ts +124 -0
  99. package/dist/transport/p2p/codec.d.ts +166 -0
  100. package/dist/transport/p2p/command-router.d.ts +655 -0
  101. package/dist/transport/p2p/commands.d.ts +550 -0
  102. package/dist/transport/p2p/envelope.d.ts +46 -0
  103. package/dist/transport/p2p/fmp4.d.ts +89 -0
  104. package/dist/transport/p2p/fragment-recording.d.ts +33 -0
  105. package/dist/transport/p2p/index.d.ts +13 -0
  106. package/dist/transport/p2p/lan-ip.d.ts +25 -0
  107. package/dist/transport/p2p/live-stream.d.ts +223 -0
  108. package/dist/transport/p2p/live-trace.d.ts +123 -0
  109. package/dist/transport/p2p/media.d.ts +105 -0
  110. package/dist/transport/p2p/p2p-session.d.ts +620 -0
  111. package/dist/transport/p2p/readable-egress.d.ts +27 -0
  112. package/dist/transport/p2p/session-manager.d.ts +154 -0
  113. package/dist/transport/p2p/shared-live-source.d.ts +431 -0
  114. package/dist/transport/p2p/talkback.d.ts +187 -0
  115. package/dist/transport/p2p/video.d.ts +150 -0
  116. package/dist/transport/p2p/write-commands.d.ts +21 -0
  117. package/dist/transport/protobuf.d.ts +5 -0
  118. package/dist/transport/push/fcm.d.ts +23 -0
  119. package/dist/transport/push/index.d.ts +6 -0
  120. package/dist/transport/push/message-tags.d.ts +26 -0
  121. package/dist/transport/push/parser.d.ts +27 -0
  122. package/dist/transport/push/proto.d.ts +11 -0
  123. package/dist/transport/push/push-client.d.ts +64 -0
  124. package/dist/transport/push/store.d.ts +23 -0
  125. package/dist/transport/push/types.d.ts +180 -0
  126. package/dist/transport/raw-dp.d.ts +6 -0
  127. package/dist/transport/stored-image-cache.d.ts +23 -0
  128. package/dist/transport/tuya/account.d.ts +44 -0
  129. package/dist/transport/tuya/client.d.ts +89 -0
  130. package/dist/transport/tuya/command-router.d.ts +79 -0
  131. package/dist/transport/tuya/dp-codec.d.ts +56 -0
  132. package/dist/transport/tuya/index.d.ts +28 -0
  133. package/dist/transport/tuya/request.d.ts +157 -0
  134. package/dist/transport/tuya/sign.d.ts +64 -0
  135. package/package.json +12 -13
  136. package/dist/client/index.js +0 -2
  137. package/dist/client/index.js.map +0 -1
  138. package/dist/core/index.js +0 -2
  139. package/dist/core/index.js.map +0 -1
  140. package/dist/model/index.js +0 -2
  141. package/dist/model/index.js.map +0 -1
  142. package/dist/transport/index.js +0 -2
  143. package/dist/transport/index.js.map +0 -1
@@ -0,0 +1,898 @@
1
+ /**
2
+ * Transport boundary contract — the shared vocabulary between the capability layer (which emits
3
+ * intent) and the transport layer (which puts bytes on a wire). It belongs to NEITHER: capabilities
4
+ * (`model/`) produce these, transports (`transport/`) consume them. Housing it in `core/` — the leaf
5
+ * both layers may import — is what lets the capability↔transport decorrelation be enforced with NO
6
+ * exceptions: `model/` never imports `transport/`, `transport/` never imports `model/`.
7
+ *
8
+ * Self-contained on purpose (no model/ or transport/ import). `LiveStreamHandle` is a STRUCTURAL
9
+ * subset of the concrete `transport/p2p/LiveStream` (which is assignable to it), so the media
10
+ * contract doesn't drag a transport type into core.
11
+ */
12
+ /** Why {@link MediaProvider.snapshotStored} has no retained push thumbnail to return. */
13
+ export type StoredSnapshotUnavailableReason = "not-observed" | "pending" | "download-failed" | "invalid-image";
14
+ /**
15
+ * Thrown by {@link MediaProvider.snapshotStored} when no validated push thumbnail is retained. The
16
+ * reason distinguishes absence, acquisition still in progress, and the latest terminal failure; the
17
+ * SDK returns only observed JPEG bytes and never substitutes live media or presentation bytes.
18
+ */
19
+ export declare class StoredSnapshotUnavailableError extends Error {
20
+ readonly reason: StoredSnapshotUnavailableReason;
21
+ constructor(reason: StoredSnapshotUnavailableReason, message: string, options?: {
22
+ cause?: unknown;
23
+ });
24
+ }
25
+ /**
26
+ * Why {@link MediaProvider.snapshotLive} could not return a still.
27
+ *
28
+ * - `no-keyframe` — no clean keyframe arrived within the acquisition window, and the live source never
29
+ * reported a failure of its own. The source may simply be slower than the window allowed.
30
+ * - `source-failed` — the live source reported a failure, so the burst will not arrive at all. Distinct
31
+ * from `no-keyframe` because it is known rather than merely elapsed, and it is known EARLY: a caller
32
+ * gets it instead of waiting out the window on a source that has already given up.
33
+ * - `undecodable-burst` — a burst was collected but the decoder refused it. Per-attempt framing, not a
34
+ * property of the camera.
35
+ * - `decoder-unavailable` — the decoder could not be run at all.
36
+ */
37
+ export type LiveSnapshotUnavailableReason = "no-keyframe" | "source-failed" | "undecodable-burst" | "decoder-unavailable";
38
+ /**
39
+ * Thrown by {@link MediaProvider.snapshotLive} when no still could be produced.
40
+ *
41
+ * {@link retryable} is the distinction the reason exists for: a burst the decoder refused is per-attempt
42
+ * framing and another try is worthwhile, while an unrunnable decoder is host configuration that no number
43
+ * of retries will change. Without it every failure looks alike.
44
+ *
45
+ * It is derived from {@link reason} rather than passed in, so the two can never disagree, and every
46
+ * caller reads one answer instead of re-deriving the mapping and drifting from it.
47
+ *
48
+ * The underlying diagnostics are preserved in the error's `message` and, where there is one, its `cause` — so
49
+ * classifying the failure never costs the detail needed to explain it.
50
+ */
51
+ export declare class LiveSnapshotUnavailableError extends Error {
52
+ readonly reason: LiveSnapshotUnavailableReason;
53
+ /** Whether another attempt could plausibly succeed without the host changing anything. */
54
+ readonly retryable: boolean;
55
+ constructor(reason: LiveSnapshotUnavailableReason, message: string, options?: {
56
+ cause?: unknown;
57
+ });
58
+ }
59
+ /**
60
+ * A media pull was refused: the camera is switched off.
61
+ *
62
+ * A disabled camera serves no live media, video or audio.
63
+ *
64
+ * Raised by every media pull on the camera surface. The pulls that answer with a promise REJECT with it;
65
+ * fragment recording answers with a handle and therefore THROWS it at the call site rather than reporting
66
+ * it on the handle. A retained push thumbnail is exempt — it is not a pull.
67
+ */
68
+ export declare class CameraDisabledError extends Error {
69
+ /** How the camera identifies itself to a reader — its name where the record carries one. */
70
+ readonly camera: string | undefined;
71
+ constructor(
72
+ /** How the camera identifies itself to a reader — its name where the record carries one. */
73
+ camera: string | undefined, options?: {
74
+ cause?: unknown;
75
+ });
76
+ }
77
+ /**
78
+ * A live stream was refused: the station is already serving another of its cameras to a viewer.
79
+ *
80
+ * A station fans several cameras out over one session and serves ONE of them at a time. Accepting a second
81
+ * live pull does not make it serve two: measured on a base carrying three attached cameras, each opened
82
+ * stream took the station from the others in turn and all three received their media in bursts. So a second
83
+ * viewer is refused rather than admitted and degraded, which is the difference between a caller being told
84
+ * the constraint and a caller watching every picture stutter.
85
+ *
86
+ * Which camera deserves the station is the caller's decision, not the SDK's, so nothing is queued or
87
+ * pre-empted here.
88
+ *
89
+ * A still is not refused: it yields the station instead, and answers with the retained image where one is
90
+ * held. Only pulls that deliver continuous media contend for a viewer's place.
91
+ */
92
+ export declare class StationBusyError extends Error {
93
+ /** The channel the station is already serving. */
94
+ readonly servingChannel: number;
95
+ /** Always true: the station is busy now, and stops being busy when the other stream is released. */
96
+ readonly retryable = true;
97
+ constructor(
98
+ /** The channel the station is already serving. */
99
+ servingChannel: number, options?: {
100
+ cause?: unknown;
101
+ });
102
+ }
103
+ /**
104
+ * How a live stream ended before its first video keyframe: the warm-up deadline elapsed, the source
105
+ * reported an error, or the source ended on its own.
106
+ */
107
+ export type LiveStreamStartFailureReason = "warm-timeout" | "source-error" | "source-ended";
108
+ /**
109
+ * How far the media source got before it failed.
110
+ *
111
+ * `awaiting-first-frame` means the source delivered nothing at all; `audio-only` means it delivered audio
112
+ * and never a video frame; `awaiting-keyframe` means video units arrived but none of them was a keyframe, so
113
+ * nothing was decodable. The three need different answers — a source that is not answering, one that is
114
+ * answering with sound and no picture, and one producing media a decoder cannot start from — and this states
115
+ * which without a caller reading transport logs.
116
+ *
117
+ * Each names only what the source did, never why. `audio-only` in particular is an observation and not a
118
+ * diagnosis: a device that is streaming but has no picture to send and one whose video this build cannot read
119
+ * both reach it.
120
+ */
121
+ export type LiveStreamStartStage = "awaiting-first-frame" | "audio-only" | "awaiting-keyframe";
122
+ /** The bounded facts a live start failure reports. */
123
+ export interface LiveStreamStartFailure {
124
+ reason: LiveStreamStartFailureReason;
125
+ stage: LiveStreamStartStage;
126
+ /** The warm-up deadline the source was bounded by, in milliseconds. */
127
+ timeoutMs: number;
128
+ /** The initial media start plus every warm-up retry issued before the deadline. */
129
+ attempts: number;
130
+ cause?: unknown;
131
+ }
132
+ /** Emitted by {@link LiveStreamHandle} when its bounded warm-up policy ends without a video keyframe. */
133
+ export declare class LiveStreamStartError extends Error {
134
+ readonly reason: LiveStreamStartFailureReason;
135
+ readonly stage: LiveStreamStartStage;
136
+ readonly timeoutMs: number;
137
+ readonly attempts: number;
138
+ constructor(failure: LiveStreamStartFailure);
139
+ }
140
+ /**
141
+ * What the observation a write declared was still waiting for when its deadline passed, carried by
142
+ * {@link StateConvergenceError}.
143
+ */
144
+ export interface StateConvergenceFailure {
145
+ sn: string;
146
+ /** The decoded property the observation names, which is what a caller reads. */
147
+ property: string;
148
+ param: number;
149
+ /** The RAW param value the write asked the device to report. */
150
+ expected?: boolean | number | string;
151
+ /** What the param actually read when the deadline passed, absent where the device reported none at all. */
152
+ observed?: boolean | number | string;
153
+ timeoutMs: number;
154
+ }
155
+ /**
156
+ * Thrown when a write's declared observation never converges, so the SDK cannot say the write landed.
157
+ *
158
+ * A command is acknowledged when the transport has carried it, which is delivery and not convergence, and
159
+ * the observation a member declares is what decides the second question. Where that observation times out
160
+ * the write was accepted by the wire and never applied by the device — measured on a battery camera whose
161
+ * power write is acknowledged and simply ignored — so this is a distinct outcome from a transport fault and
162
+ * carries the attribution that names which member on which device is unconfirmed.
163
+ */
164
+ export declare class StateConvergenceError extends Error {
165
+ readonly sn: string;
166
+ readonly property: string;
167
+ readonly param: number;
168
+ readonly expected?: boolean | number | string;
169
+ readonly observed?: boolean | number | string;
170
+ readonly timeoutMs: number;
171
+ constructor(failure: StateConvergenceFailure);
172
+ }
173
+ /**
174
+ * Wire form for a scalar {@link Command} `"set-param"` intent — how the write is sealed. The BODY is the
175
+ * same struct in every case — `[u32 channel][u32 value][account_id → 128B]` — so these choose only the
176
+ * encryption.
177
+ *
178
+ * - `"int-string"` pins level-1 (AES-128-ECB), `"direct-binary"` pins level-2 (AES-256-GCM).
179
+ * - `"auto"` lets the transport pick the level, which it resolves from the session it will send on.
180
+ */
181
+ export type ScalarForm = "auto" | "int-string" | "direct-binary";
182
+ /**
183
+ * The identity fields the `ff09` frame is built from — the AES key/IV inputs (`adminUserId`/`deviceSn`)
184
+ * plus the frame's own direction bit and user-attribution fields (`A3`/`A4`/`A5`). Shared by the
185
+ * actuate intent; the settings intents carry a narrower subset (the GET/SET frames omit the
186
+ * user-attribution fields).
187
+ */
188
+ export interface Ff09Identity {
189
+ /** Actuation direction: `true` = engage (lock / close), `false` = release (unlock / open) — the frame's `A3` byte. */
190
+ engage: boolean;
191
+ adminUserId: string;
192
+ username: string;
193
+ shortUserId: string;
194
+ deviceSn: string;
195
+ }
196
+ /** @internal */
197
+ export interface CommandObservation {
198
+ event: string;
199
+ /** The RAW param value the device must report before this write counts as landed. */
200
+ expected: boolean | number | string;
201
+ /**
202
+ * The DECODED property value to expect once that raw value has been applied, where it differs from the raw
203
+ * one. Absent when the two coincide.
204
+ *
205
+ * They diverge whenever the property's decode is not the identity: a disable-bit param reports `0` for a
206
+ * property that reads `true`, so checking the decoded value against the raw expectation would reject a write
207
+ * that had in fact landed. Stating both is what lets each check compare like with like.
208
+ */
209
+ observed?: boolean | number | string;
210
+ param: number;
211
+ property: string;
212
+ resetStandaloneSession?: boolean;
213
+ timeoutMs: number;
214
+ }
215
+ /**
216
+ * A transport-neutral outbound **command intent**. Capability modules emit one of these; they never
217
+ * call the network directly, never name a transport, and never carry a routing key. The
218
+ * {@link CommandSink} routes each by `kind`; kinds are named after the WIRE MECHANISM (the frame or
219
+ * protocol), never the capability that happens to be the first caller. Most carry an opaque param/id +
220
+ * value; a `kind` whose wire interaction is a bespoke sequence (a burst, a read-modify-write) earns its
221
+ * own variant. The `ff09-*` kinds share ONE frame that rides BOTH P2P and secure-MQTT, so the sink
222
+ * routes them by the device's runtime topology and the chosen router re-resolves its own routing tail
223
+ * from the device record — nothing transport- or route-specific lives in the intent.
224
+ */
225
+ export type Command = {
226
+ kind: "set-param";
227
+ param: number;
228
+ value: number;
229
+ form: ScalarForm;
230
+ channel: number;
231
+ } | {
232
+ kind: "set-json";
233
+ param: number;
234
+ data: Record<string, unknown>;
235
+ channel: number;
236
+ } | {
237
+ kind: "set-json-raw";
238
+ cmd: number;
239
+ data: Record<string, unknown>;
240
+ channel: number;
241
+ } | {
242
+ kind: "set-payload";
243
+ cmd: number;
244
+ payload: Record<string, unknown>;
245
+ channel: number;
246
+ mValue3?: number;
247
+ form?: ScalarForm;
248
+ } | {
249
+ kind: "p2p-privacy-burst";
250
+ enabled: boolean;
251
+ channel: number;
252
+ } | {
253
+ kind: "p2p-station-scalar";
254
+ cmd: number;
255
+ value: number;
256
+ channel: number;
257
+ }
258
+ /** P2P int-plus-string frame; the transport injects the authenticated account id string. */
259
+ | {
260
+ kind: "p2p-int-string";
261
+ cmd: number;
262
+ value: number;
263
+ valueSub: number;
264
+ channel: number;
265
+ } | ({
266
+ kind: "ff09-actuate";
267
+ } & Ff09Identity) | {
268
+ kind: "ff09-autolock";
269
+ adminUserId: string;
270
+ deviceSn: string;
271
+ enabled: boolean;
272
+ delaySeconds?: number;
273
+ } | {
274
+ kind: "ff09-setting-toggle";
275
+ adminUserId: string;
276
+ deviceSn: string;
277
+ settingId: number;
278
+ value: boolean;
279
+ } | {
280
+ kind: "mqtt-dp";
281
+ mqttCmdCode: number;
282
+ cmdCode: number;
283
+ fields: ReadonlyArray<{
284
+ tag: number;
285
+ value: Buffer;
286
+ }>;
287
+ } | {
288
+ kind: "mqtt-dp-preset";
289
+ mqttCmdCode: number;
290
+ cmdCode: number;
291
+ companionCmdCode: number;
292
+ presetId: number;
293
+ }
294
+ /** A DP custom-colour write; the transport owns RGB-to-wire conversion and field serialization. */
295
+ | {
296
+ kind: "mqtt-dp-color";
297
+ mqttCmdCode: number;
298
+ cmdCode: number;
299
+ red: number;
300
+ green: number;
301
+ blue: number;
302
+ segmentCount: number;
303
+ } | {
304
+ kind: "aiot-dp";
305
+ dp: number;
306
+ value: boolean | number | string;
307
+ };
308
+ /** Attach non-wire observation policy to a command without changing its enumerable transport intent. @internal */
309
+ export declare function observeCommand(command: Command, observation: CommandObservation): Command;
310
+ /** Read capability-owned observation policy at the client boundary. @internal */
311
+ export declare function commandObservation(command: Command): CommandObservation | undefined;
312
+ /**
313
+ * The command transport boundary. The client implements it (routing each {@link Command} `kind` to
314
+ * P2P / Tuya). Capability modules receive a sink to emit intent through, staying transport-agnostic.
315
+ */
316
+ /**
317
+ * A decoded inbound "DP" TLV frame — the transport's parse of a device→app message, handed to the
318
+ * capability layer so it can attribute MEANING to the tags without knowing the framing.
319
+ *
320
+ * The split follows the outbound direction, mirrored: a capability supplies opaque `cmdCode` + tagged
321
+ * fields and the transport frames them; inbound, the transport unwraps the envelope and validates the
322
+ * frame, and the capability decides what each tag means. Neither side needs the other's half — which is
323
+ * the whole reason this type sits on the shared floor rather than in either layer.
324
+ */
325
+ export interface DpInboundFrame {
326
+ /** The envelope's dispatch id (`head.cmd`) — capability-owned vocabulary, forwarded verbatim. */
327
+ envelopeCmd: number;
328
+ /** The frame command (`cmdHi`/`cmdLo`), likewise forwarded without interpretation. */
329
+ cmd: number;
330
+ /** The status byte a response frame carries ahead of its TLVs; absent on an unsolicited report. */
331
+ status?: number;
332
+ /** The frame's TLV run, in wire order. */
333
+ fields: ReadonlyArray<{
334
+ tag: number;
335
+ value: Buffer;
336
+ }>;
337
+ }
338
+ /**
339
+ * One top-level field of a payload decoded by a {@link RawDpCodec}, identified by its numeric position
340
+ * in the message. `kind` reports how the value was encoded, not what it means: a variable- or
341
+ * fixed-width integer arrives as `int`, and a length-delimited run — a string, a byte blob, or a nested
342
+ * message — arrives as `bytes`, for the reader to interpret.
343
+ */
344
+ export type RawDpField = {
345
+ field: number;
346
+ kind: "int";
347
+ value: bigint;
348
+ } | {
349
+ field: number;
350
+ kind: "bytes";
351
+ value: Buffer;
352
+ };
353
+ /**
354
+ * Reader for the structured, base64-encoded values some device data points carry in place of a plain
355
+ * scalar. Such a value is a length-prefixed field-and-value tree; this walks the tree and reports the
356
+ * fields it finds, with no schema and no notion of which data point the value came from.
357
+ *
358
+ * The split mirrors {@link DpInboundFrame}: decoding a container is a technical job, naming its
359
+ * contents is a semantic one. A capability receives a codec and asks for the field positions whose
360
+ * meaning it knows, so neither half has to carry the other's knowledge.
361
+ */
362
+ export interface RawDpCodec {
363
+ /**
364
+ * Decode a base64 payload to its top-level fields, or `undefined` when the value is not a
365
+ * well-formed payload — a length prefix disagreeing with the body, an encoding this does not
366
+ * recognise, or a string that is not base64 at all. The result is a whole field list or nothing,
367
+ * never a partial read.
368
+ */
369
+ decode(value: string): readonly RawDpField[] | undefined;
370
+ /** Read a length-delimited field's bytes as a nested field list, on the same terms as {@link decode}. */
371
+ nested(value: Buffer): readonly RawDpField[] | undefined;
372
+ }
373
+ export interface CommandSink {
374
+ dispatch(cmd: Command): Promise<void>;
375
+ }
376
+ /**
377
+ * Inbound Tuya DP event contract — the transport parses a ThingClips MQTT payload
378
+ * (string-keyed object → numeric-keyed record) and delivers it through this interface. The
379
+ * transport owns the parse; a capability owns the semantics (which DP id means what). Mirrors
380
+ * {@link DpInboundFrame} for the AIoT MQTT path; the capability layer never names the Tuya framing.
381
+ *
382
+ * Routing note: outbound Tuya writes use the same `aiot-dp` {@link Command} kind as AIoT MQTT;
383
+ * the facade distinguishes them by `dev.category === "eufy_home_tuya"` and routes to
384
+ * `TuyaCommandRouter` accordingly — the capability layer stays transport-agnostic.
385
+ */
386
+ export interface TuyaDpInbound {
387
+ /**
388
+ * Deliver a parsed DP event for `sn`. `dps` is numeric-keyed (converted from the wire's
389
+ * string-keyed map). The client receives this, converts to `dpParams` strings, and delivers
390
+ * via `decodeState({ source: "mqtt", dpParams })` so capability modules read Tuya values
391
+ * through the same typed getters as AIoT reports.
392
+ */
393
+ onDps(sn: string, dps: Record<number, boolean | number | string>): void;
394
+ }
395
+ /** Elementary-stream video codec of a {@link LiveVideoFrame} — eufy cameras stream H.264 or H.265. */
396
+ export type VideoCodec = "h264" | "h265" | "av1";
397
+ /**
398
+ * One decoded video access unit, as Annex-B (H.264 or H.265).
399
+ *
400
+ * A WHOLE access unit, always: a station serves a unit larger than its chunk size as several frames, and
401
+ * those are reassembled before delivery — so deciding anything per access unit (begin at a keyframe,
402
+ * switch codec at a keyframe, count frames) operates on what it says it does. A unit the transport could
403
+ * not complete is dropped rather than delivered short, because an access unit shorter than its own slice
404
+ * headers promise decodes to no picture at all.
405
+ */
406
+ export interface LiveVideoFrame {
407
+ /** True on an IDR — a unit a consumer may begin decoding at, never a continuation of an earlier one. */
408
+ keyframe: boolean;
409
+ /**
410
+ * Frame geometry as the station's own frame header states it — {@link height} is the same field.
411
+ *
412
+ * A camera reconfigures its live source WITHIN one session, so these change between frames of one
413
+ * stream. Measured on eight cameras and both codecs: four of them changed, 2 to 9 times per 25-60 s,
414
+ * oscillating up and down a ladder rather than only climbing it; the other four held one geometry
415
+ * throughout. A change arriving on a keyframe carrying fresh parameter sets is what every run but one
416
+ * showed, and that run is not accounted for, so it is not a property to rely on.
417
+ *
418
+ * This is what the station REPORTED. The size a decoder will actually produce is stated by the
419
+ * parameter sets, and a consumer is told it through {@link LiveStreamConsumer} rather than having to
420
+ * retain these and diff every frame against them.
421
+ */
422
+ width: number;
423
+ /** See {@link width} — the same field, and it moves with it. */
424
+ height: number;
425
+ /**
426
+ * Codec of the elementary stream this access unit belongs to. Sniffed off the parameter sets on a
427
+ * keyframe and carried on the delta frames that follow (a delta frame has no config to sniff).
428
+ */
429
+ codec: VideoCodec;
430
+ /** Annex-B bytes (one or more NAL units, start-code prefixed). */
431
+ data: Buffer;
432
+ }
433
+ /**
434
+ * The coded video configuration a live source is producing — the codec, and the picture size a decoder
435
+ * will produce from the parameter sets in force.
436
+ *
437
+ * The geometry is the CODED one, read out of the sequence parameter set and cropped by the offsets it
438
+ * declares, not the geometry a frame header reports. Those agreed on all but 28 of some 6000 measured
439
+ * frames, but only one of them is the size a decoder produces: 1080 is not a multiple of the 16-sample
440
+ * macroblock, so a 1080p H.264 stream codes 1088 rows and crops 8 away, and the frame header is a report
441
+ * about that rather than the definition of it.
442
+ *
443
+ * Where the parameter sets state no readable geometry — before a stream's first keyframe has carried any,
444
+ * or from a set that cannot be parsed — the frame header's report is carried instead, so a configuration is
445
+ * always present. The two are not distinguished in the payload: a configuration is acted on by comparing it
446
+ * with the one already in use, and that comparison answers the same whichever half stated it.
447
+ *
448
+ * The header's report is carried as it reads, so where the header declares no geometry either the width and
449
+ * height are `0`. A caller sizing a decoder from these treats a zero as "not yet stated" and waits for the
450
+ * next announcement, which the first keyframe's parameter sets produce.
451
+ *
452
+ * A consequence worth knowing: the first announcements of a session can move from a header-derived
453
+ * configuration to a parameter-set-derived one without the camera having reconfigured, because the sets
454
+ * arrive with the first keyframe and the frames before it have only their headers.
455
+ */
456
+ export interface LiveVideoConfig {
457
+ codec: VideoCodec;
458
+ width: number;
459
+ height: number;
460
+ }
461
+ /**
462
+ * Elementary-stream audio codec of a {@link LiveAudioFrame}. The station declares it per frame as a
463
+ * byte in the `CMD_AUDIO_FRAME` header — unlike video, nothing is sniffed. These are the three values
464
+ * the v6 app accepts (`AudioReader.setAudioSpecificConfig`: 0 → `mp4a.40.2`, 2 → G.711 A-law,
465
+ * 7 → `mp4a.40.39`); it fails the stream on anything else.
466
+ */
467
+ export type AudioCodec = "aac-lc" | "aac-eld" | "g711a";
468
+ /**
469
+ * One audio access unit, carrying the codec the station declared for it.
470
+ *
471
+ * Sample rate and channel count are deliberately absent: they are not on the wire. The v6 app assumes
472
+ * 16 kHz mono for every audio type rather than reading them, so the SDK does not invent fields the
473
+ * device never sent.
474
+ */
475
+ export interface LiveAudioFrame {
476
+ /** Codec declared in the frame header. */
477
+ codec: AudioCodec;
478
+ /** Elementary-stream bytes (ADTS-framed for the two AAC profiles). */
479
+ data: Buffer;
480
+ }
481
+ /**
482
+ * One fragmented-MP4 (CMAF) output unit from the native muxer. `init` (the `ftyp`+`moov` init
483
+ * segment) is present exactly once, on the first fragment; every fragment carries a `moof`+`mdat`
484
+ * media segment in `data`. Structural (plain `Buffer`s) so it stays in core with no transport import.
485
+ */
486
+ export interface MediaFragment {
487
+ /** The init segment (`ftyp`+`moov`), present only on the first emitted fragment. */
488
+ init?: Buffer;
489
+ /** A media fragment (`moof`+`mdat`); may be empty on the init-only first emission. */
490
+ data: Buffer;
491
+ /** Whether this fragment opens on a keyframe (a valid CMAF segment boundary). */
492
+ keyframe: boolean;
493
+ }
494
+ /**
495
+ * A fragmented-MP4 recording owned by the caller. It is an async iterable for direct `for await`
496
+ * consumption, and exposes the shared source's battery budget plus an explicit stop.
497
+ */
498
+ export interface FragmentRecordingHandle extends AsyncIterable<MediaFragment> {
499
+ /** Battery budget elapsed; call `notice.extend()` to keep the shared media session alive. */
500
+ on(event: "budget", listener: (notice: StreamBudgetNotice) => void): this;
501
+ /** End this recording and release its shared-source consumer. Idempotent. */
502
+ stop(): void;
503
+ }
504
+ /**
505
+ * Battery-budget notice for a live stream. Battery/solar cameras drain while streaming, so the source
506
+ * bounds a continuous stream to a budget; when it elapses this fires and the host decides: call
507
+ * {@link extend} to keep streaming (re-pushes the budget), or do nothing and the source auto-stops
508
+ * after a short grace to protect the battery. Wired/mains cameras never emit this — they stream
509
+ * unbounded.
510
+ */
511
+ export interface StreamBudgetNotice {
512
+ /** Milliseconds left to call {@link extend} before the source auto-stops. */
513
+ graceMs: number;
514
+ /** Re-push the budget by `ms` (default: another full budget), cancelling the pending auto-stop. */
515
+ extend(ms?: number): void;
516
+ }
517
+ /**
518
+ * The consumer-facing surface of a live stream — a STRUCTURAL subset of the concrete
519
+ * `transport/p2p/LiveStream` (an EventEmitter), so the media contract can live in core without
520
+ * importing transport. The concrete `LiveStream` is assignable to this.
521
+ */
522
+ export interface LiveStreamHandle {
523
+ start(): this;
524
+ stop(): void;
525
+ /**
526
+ * Re-issue the media-start command (start-race retry / keepalive nudge). Optional.
527
+ *
528
+ * `force` states that the channel is NOT being served, so a real start is required rather than a keepalive.
529
+ * An own-session camera sends one or the other depending on whether its session believes the channel is
530
+ * already started — a belief that outlives a station which acknowledged a start and then served nothing.
531
+ */
532
+ nudge?(force?: boolean): void;
533
+ on(event: "video", listener: (frame: LiveVideoFrame) => void): this;
534
+ on(event: "audio", listener: (frame: LiveAudioFrame) => void): this;
535
+ on(event: "start" | "stop", listener: () => void): this;
536
+ on(event: "error", listener: (err: Error) => void): this;
537
+ /** Battery-budget elapsed — extend to keep streaming or let it auto-stop (battery cameras only). */
538
+ on(event: "budget", listener: (notice: StreamBudgetNotice) => void): this;
539
+ /**
540
+ * A media start was repeated to its acknowledgement deadline and abandoned.
541
+ *
542
+ * The start is repeated byte-identically, so an abandonment is many sends with no reply — the device was
543
+ * never told to stream, and a warm-up waiting on it can only time out. Distinct from `error`: the transport
544
+ * is intact and the session is simply not being heard.
545
+ */
546
+ on(event: "unacknowledged", listener: () => void): this;
547
+ }
548
+ /**
549
+ * One consumer's view of a live stream, carrying the flow control a sink needs to apply backpressure.
550
+ *
551
+ * A sink that cannot keep up calls {@link pause}, and the frames it would have received queue against a
552
+ * bound instead of accumulating behind the sink. Crossing that bound drops the backlog and resynchronises
553
+ * at the next IDR, so a sink that stays slow resumes on decodable media rather than replaying stale media.
554
+ * {@link resume} stops the moment the sink pauses again, so the bound keeps applying to whatever is left.
555
+ *
556
+ * This is per consumer: pausing never stalls the shared source or any peer consumer.
557
+ */
558
+ export interface LiveStreamConsumer extends LiveStreamHandle {
559
+ /** True while this consumer is dropping frames after crossing its bound, waiting for the next IDR. */
560
+ readonly awaitingKeyframe: boolean;
561
+ /** Hold delivery — frames queue against the bound until {@link resume}. */
562
+ pause(): void;
563
+ /** Release delivery and hand over the queued backlog, stopping if the sink pauses again mid-drain. */
564
+ resume(): void;
565
+ /**
566
+ * The coded configuration of the video that follows, announced immediately before the first frame
567
+ * carrying it and again whenever it changes.
568
+ *
569
+ * A camera reconfigures its source repeatedly within one session, and an encoder opened for one geometry
570
+ * cannot accept a frame of another — so a consumer adapting this source to a fixed output has to rebuild
571
+ * on every change. Fires once per change rather than per frame, beginning with the first frame this
572
+ * consumer receives, so a consumer holding media it has not been told the configuration of is not a state
573
+ * it can reach.
574
+ *
575
+ * Per consumer, against what THIS consumer was last given: a consumer that joins mid-session is primed
576
+ * with a cached keyframe it did not witness arriving, and one that crosses its bound resynchronises onto
577
+ * a later IDR having skipped the frame the source saw the change on. Announcing what the source saw
578
+ * would leave both holding an encoder built for media they never received.
579
+ *
580
+ * Only a consumer announces this, never a bare {@link LiveStreamHandle}: it is read from the parameter
581
+ * sets a shared source watches every frame for, which the raw pull underneath it does not do. That is
582
+ * also why the inherited events are restated here — a subtype may add an overload only by declaring the
583
+ * whole set.
584
+ */
585
+ on(event: "video-config", listener: (config: LiveVideoConfig) => void): this;
586
+ /** One whole video access unit — see {@link LiveVideoFrame}. */
587
+ on(event: "video", listener: (frame: LiveVideoFrame) => void): this;
588
+ /** One audio access unit, in the codec the station declared for it. */
589
+ on(event: "audio", listener: (frame: LiveAudioFrame) => void): this;
590
+ on(event: "start" | "stop", listener: () => void): this;
591
+ /** Why this consumer's stream is over, including a warm-up that never produced a keyframe. */
592
+ on(event: "error", listener: (err: Error) => void): this;
593
+ /** Battery-budget elapsed — extend to keep streaming or let it auto-stop (battery cameras only). */
594
+ on(event: "budget", listener: (notice: StreamBudgetNotice) => void): this;
595
+ /**
596
+ * A media start was repeated to its acknowledgement deadline and abandoned.
597
+ *
598
+ * The start is repeated byte-identically, so an abandonment is many sends with no reply — the device was
599
+ * never told to stream, and a warm-up waiting on it can only time out. Distinct from `error`: the transport
600
+ * is intact and the session is simply not being heard.
601
+ */
602
+ on(event: "unacknowledged", listener: () => void): this;
603
+ }
604
+ /**
605
+ * What a media call tells the shared pull it may be the one to OPEN.
606
+ *
607
+ * Every egress on a device joins ONE shared pull, and whichever asks for it first is the call that builds
608
+ * it — so these are settings any egress may have to supply, and none can change once consumers are
609
+ * attached. An egress that omits one is not opting out of it; it is leaving the choice to whichever call
610
+ * got there first, which is why every egress accepts them rather than only the ones they read like.
611
+ */
612
+ /**
613
+ * How a caller abandons ONE media call, without touching the shared pull other callers hold.
614
+ *
615
+ * Acquiring media can wait a long time before it can succeed or fail: a station has to connect, a level-2
616
+ * key has to be negotiated or given up on, and a camera has to produce a keyframe. Measured at twenty
617
+ * seconds and more on a battery camera.
618
+ *
619
+ * Aborting settles the call with an `AbortError` and gives back whatever it had taken, so a pull nothing
620
+ * else holds is released rather than left running for a caller that has gone. It never disturbs a pull
621
+ * another consumer is attached to: this abandons a call, not a stream.
622
+ *
623
+ * Separate from {@link SharedSourceHints} on purpose. Those describe the pull a call may open and are
624
+ * fixed for everyone who joins it; this belongs to one call and to nobody else.
625
+ */
626
+ export interface AbortableCall {
627
+ signal?: AbortSignal;
628
+ }
629
+ export interface SharedSourceHints {
630
+ /**
631
+ * Power source, a runtime device fact (`"battery"` incl. solar, or `"wired"`) — never a device-family
632
+ * trait. `"wired"` streams unbounded; `"battery"` bounds a continuous stream to a budget, after which
633
+ * the handle's `budget` notice offers an extension.
634
+ */
635
+ powered?: "wired" | "battery";
636
+ /**
637
+ * Seconds of already-captured media the pull retains for a later drain; `0`, absent, non-finite and
638
+ * negative values retain none.
639
+ *
640
+ * Retention costs memory on every frame and only a caller knows whether anything will drain it, so it
641
+ * is never assumed. Media can only be retained while the pull is running, so a window answers "what did
642
+ * the camera capture just before this" only for a pull something already opened.
643
+ */
644
+ preBufferSeconds?: number;
645
+ }
646
+ /**
647
+ * The **media / device-query boundary** — the second transport, for operations that RETURN data (a
648
+ * still, a live stream, a recording, a P2P request/reply query). The client implements it (P2P media
649
+ * plumbing); capability modules call it without knowing the protocol. Bound to one device serial, so
650
+ * methods take none. `p2pQuery` is a GENERIC request/reply primitive (transport only). Optional
651
+ * because an unbound model has no live client (hence `?.` at the call site).
652
+ */
653
+ export interface MediaProvider {
654
+ /**
655
+ * Return the latest validated push thumbnail retained in memory. This passive operation performs no
656
+ * network, storage, P2P, live-media, or transcoding work at call time. It rejects with
657
+ * {@link StoredSnapshotUnavailableError} when no image is retained. Optional because cache ownership
658
+ * and capability binding belong to the client.
659
+ */
660
+ snapshotStored?(): Promise<Buffer>;
661
+ /**
662
+ * A fresh still decoded from a short live burst.
663
+ *
664
+ * `width`/`height` describe the RETURNED IMAGE, read back out of it rather than taken from the stream's
665
+ * frame header: the header states the geometry at capture start, and a camera whose stream reconfigures
666
+ * mid-burst leaves it contradicting the bytes — which a caller sizing a buffer or caching by resolution
667
+ * cannot detect short of parsing the JPEG itself.
668
+ *
669
+ * Rejects with {@link LiveSnapshotUnavailableError}, whose {@link LiveSnapshotUnavailableError.retryable}
670
+ * says whether another attempt could succeed.
671
+ *
672
+ * Carries {@link SharedSourceHints} for the reason stated there: a still polled on an idle camera is
673
+ * routinely the call that OPENS the shared pull, so it decides the power budget and the retained window
674
+ * for every egress that joins later.
675
+ */
676
+ snapshotLive(opts?: {
677
+ timeoutMs?: number;
678
+ collectMs?: number;
679
+ skipKeyframes?: number;
680
+ } & SharedSourceHints & AbortableCall): Promise<{
681
+ jpeg: Buffer;
682
+ width: number;
683
+ height: number;
684
+ /**
685
+ * Present and `true` only when these bytes are the RETAINED still rather than a fresh capture.
686
+ *
687
+ * A live still is refused while a sibling camera on the same station is being watched, because a
688
+ * station serves one camera at a time and the live view is the picture someone is looking at. Answering
689
+ * the retained still there answers the call instead of failing it, and this says the bytes are not
690
+ * current. Absent means freshly captured.
691
+ */
692
+ retained?: true;
693
+ }>;
694
+ /**
695
+ * Open a managed live stream.
696
+ *
697
+ * Several cameras behind one station may stream at the same time only where the station serves them at
698
+ * the same time. Where it serves one camera at a time, a second viewer is refused with
699
+ * {@link StationBusyError} rather than admitted and degraded: accepting it does not make the station
700
+ * serve two, it makes both stutter. Which camera deserves the station is the caller's decision, so
701
+ * nothing is queued or pre-empted. Each handle receives only the frames the station tagged for ITS
702
+ * camera.
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * const stream = await cam.live();
707
+ * stream.on("video", (frame) => write(frame.data)); // Annex-B
708
+ * stream.stop(); // detach this consumer
709
+ * ```
710
+ */
711
+ live(opts?: SharedSourceHints & AbortableCall & Record<string, unknown>): Promise<LiveStreamConsumer>;
712
+ /**
713
+ * Record `seconds` of video → an mp4/h264 buffer.
714
+ *
715
+ * Opens its OWN pull rather than joining the shared source, so it costs a second stream on a camera that
716
+ * is already streaming. {@link recordFragments} is a shared consumer like every other egress.
717
+ *
718
+ * Always settles: it resolves once the requested window has elapsed — with the run the camera actually
719
+ * delivered inside it, which a camera that goes quiet mid-clip makes shorter than asked — and rejects when
720
+ * the pull fails or ends before that window is up, or when no keyframe arrives to start the clip at.
721
+ */
722
+ record(seconds: number, opts?: {
723
+ timeoutMs?: number;
724
+ skipKeyframes?: number;
725
+ }): Promise<Buffer>;
726
+ /**
727
+ * Open a video-only `node:stream` Readable over a shared source consumer — raw Annex-B bytes
728
+ * (default) or `objectMode` {@link LiveVideoFrame}s. Audio is available separately through
729
+ * {@link live} or muxed through {@link recordFragments}; it is never interleaved into raw video.
730
+ * The caller owns the Readable's lifetime, and destroying it releases the shared pull.
731
+ */
732
+ openReadable?(opts?: {
733
+ objectMode?: boolean;
734
+ } & SharedSourceHints & AbortableCall): Promise<import("node:stream").Readable>;
735
+ /**
736
+ * Continuously record the live feed as fragmented-MP4 (CMAF). The caller-owned
737
+ * {@link FragmentRecordingHandle} yields an init segment then keyframe-bounded media fragments,
738
+ * emits battery-budget notices, and releases the shared pull on `stop`, `break`, or `return`.
739
+ *
740
+ * {@link SharedSourceHints.preBufferSeconds} does double duty here: it configures the retained window
741
+ * when this call is the one that opens the pull, and it is the length this recording drains from a pull
742
+ * that was already open. The drain opens on the newest keyframe at or before the window starts, so it
743
+ * covers the request and exceeds it by however far back that keyframe sits.
744
+ */
745
+ recordFragments?(opts?: {
746
+ fragmentSeconds?: number;
747
+ } & SharedSourceHints & AbortableCall): FragmentRecordingHandle;
748
+ /**
749
+ * Open the camera's **talkback** path — audio travelling from the host TO the device, the opposite
750
+ * direction to everything else here. See {@link TalkbackHandle} for the accepted audio. Optional (an
751
+ * unbound model has no client), and absent on a device whose talkback wire is unverified.
752
+ */
753
+ talkback?(opts?: {
754
+ encoder?: AacEncoder;
755
+ } & SharedSourceHints): Promise<TalkbackHandle>;
756
+ /**
757
+ * Generic P2P request/reply query: send a `SET_PAYLOAD` sub-command and resolve with the reply
758
+ * frame's `payload` (the reply whose `cmd` echoes `subCmd`). Transport-only — the caller owns the
759
+ * sub-command id and the reply shape (e.g. the doorbell's 6237 quick-response list).
760
+ * @internal
761
+ */
762
+ p2pQuery?(subCmd: number, opts?: {
763
+ timeoutMs?: number;
764
+ }): Promise<Record<string, unknown>>;
765
+ /**
766
+ * Generic control-payload request/reply query: send a `CONTROL_PAYLOAD` (1700) `{commandType,data}`
767
+ * and resolve with the correlated notify (`1351`) frame's `payload`. Distinct from {@link p2pQuery}'s
768
+ * `SET_PAYLOAD` (1350) envelope — some queries ride the 1700 wrapper with a 1351 reply instead.
769
+ * Transport-only; the caller owns the command id, request data, and reply shape.
770
+ * @internal
771
+ */
772
+ p2pControlQuery?(param: number, data: Record<string, unknown>, opts?: {
773
+ timeoutMs?: number;
774
+ }): Promise<Record<string, unknown>>;
775
+ }
776
+ /**
777
+ * An encoder that turns raw PCM into AAC-LC frames, supplied by the CALLER. The SDK ships none: the
778
+ * device's audio path is fixed at AAC-LC 16 kHz mono, and every plausible encoder is either a native
779
+ * dependency or an external process, both of which belong to the host rather than to a protocol SDK.
780
+ * It is unnecessary where the audio is already AAC — see {@link TalkbackHandle}.
781
+ *
782
+ * `encode` receives 16-bit little-endian mono PCM at 16 kHz and returns whole ADTS frames, zero or
783
+ * more per call (an encoder buffers until it has a full 1024-sample block). `flush` drains a partial
784
+ * trailing block; `close` releases whatever the encoder holds.
785
+ */
786
+ export interface AacEncoder {
787
+ encode(pcm: Buffer): Buffer[] | Promise<Buffer[]>;
788
+ flush?(): Buffer[] | Promise<Buffer[]>;
789
+ close?(): void;
790
+ }
791
+ /**
792
+ * A live talkback session: audio pushed from the host to a camera's speaker, the mirror of
793
+ * {@link LiveStreamHandle}'s inbound feed.
794
+ *
795
+ * Audio must be **AAC-LC, 16 kHz, mono, in ADTS frames** — what the device's path is fixed at, so a
796
+ * stream at another rate or channel count is rejected rather than resampled (it would otherwise play
797
+ * at the wrong pitch and speed). Feed it either way:
798
+ *
799
+ * - **ADTS AAC** — the default. Chunk boundaries are irrelevant; frames are recovered from the
800
+ * stream, so piping an encoder's output straight in works.
801
+ * - **PCM** — only when the handle was opened with an {@link AacEncoder}, which then does the
802
+ * conversion. `write` takes 16-bit little-endian mono PCM at 16 kHz instead.
803
+ *
804
+ * Frames are **paced** at their own playback rate (64 ms each) rather than flushed as fast as they
805
+ * arrive, so feeding a file plays it at speed instead of overrunning the device. A live source keeps
806
+ * the queue near-empty and is unaffected.
807
+ *
808
+ * @example
809
+ * ```ts
810
+ * const talk = await cam.talkback!();
811
+ * talk.on("error", (err) => console.error(err.message));
812
+ * talk.on("finished", () => void talk.stop());
813
+ * fs.createReadStream("greeting.aac").pipe(talk.writable());
814
+ * ```
815
+ */
816
+ export interface TalkbackHandle {
817
+ /** Queue audio — ADTS frames, or PCM when an encoder was supplied. Partial frames are held. */
818
+ write(chunk: Buffer): void;
819
+ /**
820
+ * A `node:stream` Writable over {@link write}, for piping a file or an encoder's stdout. Applies
821
+ * backpressure while the pacing queue is full, so a fast source cannot outrun playback.
822
+ */
823
+ writable(): import("node:stream").Writable;
824
+ /**
825
+ * Declare the input finished, so a drained queue can report the clip complete. `writable()` calls
826
+ * this from its `final`, so a piped source needs no explicit call; an imperative {@link write}
827
+ * caller does. Writing after this is an `error`, not more audio — open a new talkback for a new clip.
828
+ */
829
+ end(): void;
830
+ /** How many frames are still queued for the wire — `0` once everything written has reached it. */
831
+ readonly pending: number;
832
+ /** Close the path, dropping anything still queued. Idempotent. */
833
+ stop(): Promise<void>;
834
+ /**
835
+ * The clip is complete: the input has ended (via {@link end} or the writable's `final`) AND every
836
+ * queued frame has reached the wire. Fires once — the natural moment to {@link stop} a finite clip.
837
+ *
838
+ * This deliberately does NOT fire on a merely-empty queue. A realtime source keeps the queue near
839
+ * empty by design, so "queue is empty" arrives after the very first frame and stopping on it would
840
+ * cut the clip to 64 ms. {@link pending} is the instantaneous depth.
841
+ */
842
+ on(event: "finished", listener: () => void): this;
843
+ /**
844
+ * The path closed — either {@link stop} was called, or the media session it rides inside ended and
845
+ * took it with it. It always fires exactly once, so it is a teardown hook rather than a signal that
846
+ * something went wrong.
847
+ */
848
+ on(event: "stop", listener: () => void): this;
849
+ /**
850
+ * A frame the device would not play (wrong sample rate or channel count, or over its length limit),
851
+ * an encoder failure, or an audio frame the device never acknowledged — the channel is ordered, so
852
+ * an unacknowledged frame can stall playback behind it. Non-fatal: the session stays open.
853
+ */
854
+ on(event: "error", listener: (err: Error) => void): this;
855
+ /**
856
+ * Battery cameras only: the media session talkback rides inside has reached its power budget and
857
+ * will auto-stop after the notice's grace period, taking the audio with it. Call `extend()` to keep
858
+ * talking. Without a listener the session stops on schedule, which protects the battery.
859
+ */
860
+ on(event: "budget", listener: (notice: StreamBudgetNotice) => void): this;
861
+ }
862
+ /**
863
+ * Decoded auto-lock settings, read off the device's settings reply (the response
864
+ * tag map: `a1`=enabled, `a2`=delaySeconds, `a3`=isSchedule, `a4`/`a5`=schedule start/end). These are
865
+ * the SAME fields `setAutoLock` already reads internally to preserve them on a write — this is that
866
+ * read, exposed standalone with no write attached.
867
+ */
868
+ export interface AutoLockSnapshot {
869
+ /** Whether auto-lock is currently enabled. */
870
+ enabled: boolean;
871
+ /** Auto-lock delay, in seconds. */
872
+ delaySeconds: number;
873
+ /** Whether the schedule window is active. */
874
+ isSchedule: boolean;
875
+ /** Schedule start, `[hour, minute]` — read back verbatim, not independently validated. */
876
+ scheduleStartTime: [number, number];
877
+ /** Schedule end, `[hour, minute]` — read back verbatim, not independently validated. */
878
+ scheduleEndTime: [number, number];
879
+ }
880
+ /**
881
+ * The **`ff09` settings read boundary** — `GET_SETTINGS` is a request/reply query (like a
882
+ * `MediaProvider` media op), not a passive property the device broadcasts, so it needs its own
883
+ * request/reply primitive rather than reusing the write-only {@link CommandSink}. Named for the frame
884
+ * family it reads, the same way the `ff09-*` {@link Command} kinds and {@link Ff09Identity} are: any
885
+ * device driven by that frame can grow a reader here, and the settings it exposes are the frame's, not
886
+ * one capability's.
887
+ *
888
+ * Bound to one device serial (transport picked internally, P2P or MQTT, same as `CommandSink`), so the
889
+ * methods take no identity args. Optional because an unbound model has no live client, and because only
890
+ * `ff09`-family devices have one at all.
891
+ *
892
+ * One method today. A further `ff09` setting that needs a live read is another method here — not
893
+ * another injected provider.
894
+ */
895
+ export interface Ff09SettingsReader {
896
+ /** Read the device's current auto-lock settings via a live `GET_SETTINGS` round-trip. */
897
+ getAutoLockState(): Promise<AutoLockSnapshot>;
898
+ }