@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.
- package/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- 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
|
+
}
|