@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed talkback session over P2P — the send-side twin of `live-stream.ts`. Takes AAC-LC audio from
|
|
3
|
+
* a host, recovers frame boundaries, and paces the frames onto the wire at their own playback rate,
|
|
4
|
+
* bracketed by the device's start/stop control frames.
|
|
5
|
+
*
|
|
6
|
+
* Pacing is the reason this exists rather than a bare `sendAudioFrame` loop. Each AAC-LC frame at
|
|
7
|
+
* 16 kHz is 1024 samples — exactly 64 ms of audio — and the device plays what arrives when it arrives.
|
|
8
|
+
* A host piping a file would otherwise deliver a minute of audio in a few hundred milliseconds and
|
|
9
|
+
* lose all but the tail. A live source paces itself and simply keeps the queue near-empty.
|
|
10
|
+
*
|
|
11
|
+
* The pacer is **wall-clock**, not tick-counted: a timer callback that runs late releases every frame
|
|
12
|
+
* it owes rather than one. Releasing one per tick loses each late tick permanently, which measures as a
|
|
13
|
+
* feed slower than realtime on a loaded event loop — and this feature attaches to a shared live source,
|
|
14
|
+
* so muxing video on the same loop is the normal case, not the edge case.
|
|
15
|
+
*
|
|
16
|
+
* @module p2p/talkback
|
|
17
|
+
*/
|
|
18
|
+
import { EventEmitter } from "node:events";
|
|
19
|
+
import { Writable } from "node:stream";
|
|
20
|
+
import type { AacEncoder, StreamBudgetNotice, TalkbackHandle } from "../../core/contracts.js";
|
|
21
|
+
import { type Logger } from "../../core/logger.js";
|
|
22
|
+
/**
|
|
23
|
+
* The subset of the session this needs — start/stop the path, push one frame, and report a frame the
|
|
24
|
+
* device never acknowledged. The audio channel is ordered, so an abandoned frame is a permanent hole
|
|
25
|
+
* that stalls everything behind it; the listener pair lets that surface instead of going silent.
|
|
26
|
+
*/
|
|
27
|
+
export interface TalkbackSink {
|
|
28
|
+
startTalkback(channel: number, homeBaseAttached: boolean): boolean;
|
|
29
|
+
stopTalkback(channel: number, homeBaseAttached: boolean): boolean;
|
|
30
|
+
sendAudioFrame(channel: number, frame: Buffer): void;
|
|
31
|
+
on?(event: "audioGap", listener: (seq: number) => void): unknown;
|
|
32
|
+
off?(event: "audioGap", listener: (seq: number) => void): unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface TalkbackOptions {
|
|
35
|
+
channel: number;
|
|
36
|
+
homeBaseAttached: boolean;
|
|
37
|
+
/** Converts PCM to ADTS when the caller feeds PCM instead of AAC. */
|
|
38
|
+
encoder?: AacEncoder;
|
|
39
|
+
/**
|
|
40
|
+
* Releases the media session this talkback is riding inside. Called once on {@link Talkback.stop}.
|
|
41
|
+
* The device only plays host audio while its media session is open (verified live: the identical
|
|
42
|
+
* frames are silent without one), so the session is held for the talkback's whole lifetime.
|
|
43
|
+
*/
|
|
44
|
+
releaseMedia?(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Frames the pacing queue holds before `writable()` applies backpressure. The default is two
|
|
47
|
+
* seconds of audio — enough to ride out scheduler jitter without letting a file source buffer the
|
|
48
|
+
* whole clip in memory.
|
|
49
|
+
*/
|
|
50
|
+
highWaterFrames?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Stop by itself after this long with nothing written. A handle holds a consumer on the shared live
|
|
53
|
+
* source and a repeating timer, and neither is reaped by anything but an explicit
|
|
54
|
+
* {@link Talkback.stop} — a battery camera's power budget does not apply to a wired one. A caller
|
|
55
|
+
* that drops the handle would otherwise pin a P2P pull for the life of the process. Pass `0` to
|
|
56
|
+
* disable. Default {@link DEFAULT_IDLE_TIMEOUT_MS}.
|
|
57
|
+
*/
|
|
58
|
+
idleTimeoutMs?: number;
|
|
59
|
+
logger?: Logger;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How long a talkback waits for audio before stopping itself. Comfortably longer than any gap a live
|
|
63
|
+
* or press-to-talk source produces mid-sentence, and short enough that an abandoned handle does not
|
|
64
|
+
* outlive the conversation it belonged to.
|
|
65
|
+
*/
|
|
66
|
+
export declare const DEFAULT_IDLE_TIMEOUT_MS = 30000;
|
|
67
|
+
/**
|
|
68
|
+
* A talkback session bound to one camera channel. Opens the device's path on construction via
|
|
69
|
+
* {@link start}, then drains queued frames on a fixed 64 ms tick until {@link stop}.
|
|
70
|
+
*/
|
|
71
|
+
export declare class Talkback extends EventEmitter implements TalkbackHandle {
|
|
72
|
+
private readonly sink;
|
|
73
|
+
private readonly opts;
|
|
74
|
+
private readonly reader;
|
|
75
|
+
private readonly queue;
|
|
76
|
+
private readonly logger;
|
|
77
|
+
private readonly highWater;
|
|
78
|
+
private readonly idleTimeoutMs;
|
|
79
|
+
private timer?;
|
|
80
|
+
private started;
|
|
81
|
+
private stopped;
|
|
82
|
+
private ended;
|
|
83
|
+
private announcedFinish;
|
|
84
|
+
private onRoom?;
|
|
85
|
+
/** When the next frame is due, in `Date.now()` terms — the pacer's wall-clock cursor. */
|
|
86
|
+
private nextDueAt;
|
|
87
|
+
private lastWriteAt;
|
|
88
|
+
/**
|
|
89
|
+
* Serializes the encoder path. `write()` must not block a synchronous caller, so an encode runs
|
|
90
|
+
* detached — but two detached encodes can settle out of order, and both push into a STATEFUL
|
|
91
|
+
* {@link AdtsFrameReader}. Chaining keeps the frames in the order they were written and keeps the
|
|
92
|
+
* end-of-input flush behind the last pending encode.
|
|
93
|
+
*/
|
|
94
|
+
private chain;
|
|
95
|
+
private readonly onAudioGap;
|
|
96
|
+
constructor(sink: TalkbackSink, opts: TalkbackOptions);
|
|
97
|
+
/** Frames still queued for the wire. */
|
|
98
|
+
get pending(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Open the device's talkback path and begin the pacing tick. Throws when the topology's control
|
|
101
|
+
* frame could not be sent — for a HomeBase-attached camera that means the level-2 key was never
|
|
102
|
+
* negotiated, which would otherwise leave a silent session that accepts audio nobody hears.
|
|
103
|
+
*/
|
|
104
|
+
start(): this;
|
|
105
|
+
/**
|
|
106
|
+
* Queue audio. Bytes are ADTS AAC, or PCM when an encoder was supplied; either way the input is
|
|
107
|
+
* treated as a stream, so a chunk carrying part of a frame is held until the rest arrives.
|
|
108
|
+
*
|
|
109
|
+
* Audio written after {@link end} is dropped with an `error` rather than queued: `finished` has
|
|
110
|
+
* either already fired or is owed on the frames written before it, so a late chunk would either play
|
|
111
|
+
* after the clip was declared complete or never be announced at all.
|
|
112
|
+
*/
|
|
113
|
+
write(chunk: Buffer): void;
|
|
114
|
+
/**
|
|
115
|
+
* Declare the input finished. Nothing more may be written; once the queue drains, `finished` fires.
|
|
116
|
+
* `writable()` calls this from its `final`, so a piped source needs no explicit call.
|
|
117
|
+
*/
|
|
118
|
+
end(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Run PCM through the caller's encoder. Kept off {@link write}'s return path so a synchronous
|
|
121
|
+
* caller isn't forced to await; an encoder failure surfaces as an `error` event.
|
|
122
|
+
*/
|
|
123
|
+
private encodeAndEnqueue;
|
|
124
|
+
/**
|
|
125
|
+
* Admit whole frames to the pacing queue, rejecting any whose parameters the device cannot play or
|
|
126
|
+
* whose length it refuses. A rejected frame is dropped with an `error` rather than silently
|
|
127
|
+
* swallowed: passing it through would produce audible garbage.
|
|
128
|
+
*/
|
|
129
|
+
private enqueue;
|
|
130
|
+
/**
|
|
131
|
+
* Release every frame the wall clock says is due, then report the clip finished once the input has
|
|
132
|
+
* ended and nothing is left. Pacing is unconditional: it tracks the audio's own playback rate and
|
|
133
|
+
* never waits on the device's acknowledgements, because the device is playing a continuous stream
|
|
134
|
+
* and a slowed feed starves it.
|
|
135
|
+
*
|
|
136
|
+
* The cursor advances by one frame's duration per frame sent rather than being reset to `now`, so a
|
|
137
|
+
* timer that fires late sends what it owes and the feed stays at realtime. When the queue runs dry
|
|
138
|
+
* the cursor is re-based one frame ahead of `now`: the silence was the source's, and owing frames for
|
|
139
|
+
* it would burst them the moment audio resumes.
|
|
140
|
+
*/
|
|
141
|
+
private tick;
|
|
142
|
+
/**
|
|
143
|
+
* Stop a talkback nothing is feeding any more. Only armed while the queue is empty — a source that
|
|
144
|
+
* is merely slow keeps frames in flight, and a live source pushes on its own cadence. It covers a
|
|
145
|
+
* finished clip too: a caller that listens for `finished` but never stops still holds the shared
|
|
146
|
+
* consumer, which is the same leak as dropping the handle outright.
|
|
147
|
+
*/
|
|
148
|
+
private idleCheck;
|
|
149
|
+
/** Emit `finished` once, when the input has ended and every queued frame has reached the wire. */
|
|
150
|
+
private announceFinishIfDone;
|
|
151
|
+
/** Hand a withheld `writable()` callback back, so the source resumes. */
|
|
152
|
+
private releaseWriter;
|
|
153
|
+
/**
|
|
154
|
+
* Report a failure without being able to kill the host. `error` on an `EventEmitter` THROWS when
|
|
155
|
+
* nothing is listening, and every call site here is either inside the pacing interval or on a
|
|
156
|
+
* detached encode — where that throw aborts the process rather than reaching a caller. The common
|
|
157
|
+
* case is audio that simply isn't 16 kHz mono, which must not take the process down.
|
|
158
|
+
*/
|
|
159
|
+
private fail;
|
|
160
|
+
/**
|
|
161
|
+
* A Writable over {@link write} that withholds its callback while the queue is at the high-water
|
|
162
|
+
* mark — which is what turns `pipe()` from a file into playback at speed rather than a memory spike.
|
|
163
|
+
*/
|
|
164
|
+
writable(): Writable;
|
|
165
|
+
/** Drain an encoder's partial trailing block, so the last syllable is not lost. */
|
|
166
|
+
private flushEncoder;
|
|
167
|
+
/**
|
|
168
|
+
* Close the path: stop pacing, drop anything still queued, and send the stop control frame. Queued
|
|
169
|
+
* audio is deliberately discarded — `finished` is the event that says everything queued has already
|
|
170
|
+
* reached the wire.
|
|
171
|
+
*
|
|
172
|
+
* Everything after the guard runs under `try`/`finally` because the teardown calls back into
|
|
173
|
+
* caller-supplied code (the encoder's `close`, a withheld stream callback, the session's stop frame),
|
|
174
|
+
* any of which can throw. `releaseMedia` must run regardless: the idempotence guard has already
|
|
175
|
+
* latched by then, so a skipped release can never be retried, and the shared consumer would stay
|
|
176
|
+
* attached — holding the P2P pull open until the process exits.
|
|
177
|
+
*/
|
|
178
|
+
stop(): Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
export interface Talkback {
|
|
181
|
+
on(event: "finished" | "stop", listener: () => void): this;
|
|
182
|
+
on(event: "error", listener: (err: Error) => void): this;
|
|
183
|
+
on(event: "budget", listener: (notice: StreamBudgetNotice) => void): this;
|
|
184
|
+
emit(event: "finished" | "stop"): boolean;
|
|
185
|
+
emit(event: "error", err: Error): boolean;
|
|
186
|
+
emit(event: "budget", notice: StreamBudgetNotice): boolean;
|
|
187
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Parsed fields of a `CMD_VIDEO_FRAME` 22-byte header. */
|
|
2
|
+
export interface VideoFrameHeader {
|
|
3
|
+
/**
|
|
4
|
+
* Length of the body THIS frame carries, from u32 LE @ 0x00 — not the length of the access unit it
|
|
5
|
+
* belongs to.
|
|
6
|
+
*
|
|
7
|
+
* A station serves a unit larger than {@link STATION_CHUNK_BYTES} as several frames, and each one
|
|
8
|
+
* declares only its own share: measured on two models, a 70190-byte unit arrived as `64000` then
|
|
9
|
+
* `6190`, and a 104451-byte keyframe as `64000` then `40451`. A frame that carries a whole unit
|
|
10
|
+
* declares the whole unit, however large — a captured keyframe declares 0x3b323.
|
|
11
|
+
*/
|
|
12
|
+
payloadLength: number;
|
|
13
|
+
/** True when flag@0x04 bit0 is set: a keyframe carrying the ECIES envelope. */
|
|
14
|
+
keyframe: boolean;
|
|
15
|
+
/** Raw flag byte at 0x04. */
|
|
16
|
+
flags: number;
|
|
17
|
+
/**
|
|
18
|
+
* u16 LE @ 0x06 — repeated across every frame of ONE access unit, which is what makes it usable as
|
|
19
|
+
* part of a unit's identity. What it counts differs by model (one increments it per unit, another
|
|
20
|
+
* leaves it 0 for the whole stream), so it is never read as a count on its own.
|
|
21
|
+
*/
|
|
22
|
+
sequence: number;
|
|
23
|
+
/** Frame width (s16 LE @ 0x0a). */
|
|
24
|
+
width: number;
|
|
25
|
+
/** Frame height (s16 LE @ 0x0c). */
|
|
26
|
+
height: number;
|
|
27
|
+
/** Timestamp word (u32 LE @ 0x0e). */
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
/** A successfully decoded video frame. */
|
|
31
|
+
export interface DecodedVideoFrame {
|
|
32
|
+
/** True if this was a keyframe (IDR) — its envelope re-keyed the decoder. */
|
|
33
|
+
keyframe: boolean;
|
|
34
|
+
/** Decrypted H.264 elementary-stream bytes. */
|
|
35
|
+
h264: Buffer;
|
|
36
|
+
/** Frame width in pixels. */
|
|
37
|
+
width: number;
|
|
38
|
+
/** Frame height in pixels. */
|
|
39
|
+
height: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse the 22-byte `CMD_VIDEO_FRAME` header. Does not validate lengths beyond the header itself,
|
|
43
|
+
* so it is safe to call on a (sufficiently long) buffer to inspect frame metadata without keys.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseVideoFrameHeader(payload: Buffer): VideoFrameHeader | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Decodes a single P2P video stream into H.264. Construct one per stream with the camera's 32-byte
|
|
48
|
+
* ECC private key; feed it `CMD_VIDEO_FRAME` payloads in order. Keyframes re-key the decoder via
|
|
49
|
+
* their ECIES envelope; P/B-frames reuse the cached media key. All failure modes (short/garbage
|
|
50
|
+
* frame, wrong key, tampered HMAC, GCM auth failure) return `undefined` — the decoder never throws.
|
|
51
|
+
*/
|
|
52
|
+
export declare class VideoFrameDecoder {
|
|
53
|
+
private readonly eccPrivateKeyHex;
|
|
54
|
+
private mediaKey?;
|
|
55
|
+
/** @param eccPrivateKey the camera's 32-byte ECC private key (P-256 scalar). */
|
|
56
|
+
constructor(eccPrivateKey: Buffer);
|
|
57
|
+
/** The media key recovered from the most recent keyframe, if any. */
|
|
58
|
+
get currentMediaKey(): Buffer | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Recover the per-stream media key from a keyframe's 129-byte ECIES envelope: ECIES unwrap
|
|
61
|
+
* (ECDH → eufyKDF → AES-128-CBC, with the trailing HMAC verified) → 32-byte AES-256-GCM key.
|
|
62
|
+
*/
|
|
63
|
+
private unwrapMediaKey;
|
|
64
|
+
/**
|
|
65
|
+
* Decode one video frame. Returns the keyframe flag and decrypted H.264 bytes, or `undefined`
|
|
66
|
+
* if the frame is malformed, the envelope/key is wrong, or GCM authentication fails.
|
|
67
|
+
*/
|
|
68
|
+
decodeFrame(payload: Buffer): DecodedVideoFrame | undefined;
|
|
69
|
+
}
|
|
70
|
+
/** One whole access unit recovered from the frame (or frames) the station sent it in. */
|
|
71
|
+
export interface AssembledAccessUnit {
|
|
72
|
+
/** True when the unit's own header flagged it a keyframe — a point a consumer may begin decoding at. */
|
|
73
|
+
keyframe: boolean;
|
|
74
|
+
/** Frame width the unit's header declared. */
|
|
75
|
+
width: number;
|
|
76
|
+
/** Frame height the unit's header declared. */
|
|
77
|
+
height: number;
|
|
78
|
+
/** The complete payload, as long as the header said it would be. */
|
|
79
|
+
data: Buffer;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The payload size a station fills a `CMD_VIDEO_FRAME` to before splitting an access unit across
|
|
83
|
+
* several of them.
|
|
84
|
+
*
|
|
85
|
+
* Verified on two independent camera models on one account: every frame of a split unit declared
|
|
86
|
+
* EXACTLY this, and the unit's last frame declared less.
|
|
87
|
+
*
|
|
88
|
+
* Matched by equality, never as a floor. Stations that do not split deliver whole units in one frame far
|
|
89
|
+
* above this — measured at 148057, 231954 and 234670 bytes on three other models — and treating "at least
|
|
90
|
+
* this big" as "more is coming" holds those units back and then discards them as truncated, which costs
|
|
91
|
+
* exactly the keyframes a decoder cannot start without.
|
|
92
|
+
*/
|
|
93
|
+
export declare const STATION_CHUNK_BYTES = 64000;
|
|
94
|
+
/**
|
|
95
|
+
* Reassembles a `CMD_VIDEO_FRAME` access unit the station split across several frames.
|
|
96
|
+
*
|
|
97
|
+
* Each frame declares only the payload IT carries ({@link VideoFrameHeader.payloadLength}), so the unit's
|
|
98
|
+
* total length is nowhere on the wire and cannot be waited for. What the wire does carry, verified on two
|
|
99
|
+
* models:
|
|
100
|
+
*
|
|
101
|
+
* - the frames of one unit **repeat its header** — same timestamp, same {@link VideoFrameHeader.sequence} —
|
|
102
|
+
* while consecutive units differ in both;
|
|
103
|
+
* - every frame of a split unit is filled to {@link STATION_CHUNK_BYTES} except the last;
|
|
104
|
+
* - a frame that STARTS a unit begins with an Annex-B start code once decoded, and a continuation begins
|
|
105
|
+
* mid-NAL, so it does not.
|
|
106
|
+
*
|
|
107
|
+
* A unit is therefore held only while its latest frame is full, and completed the moment a shorter one
|
|
108
|
+
* arrives — no delivery latency for the overwhelming majority of units, which arrive in one frame below
|
|
109
|
+
* the threshold. The identity match and the missing start code are required TOGETHER before appending:
|
|
110
|
+
* either alone would let two decodable units merge, and a merge is invisible to a consumer that trusts
|
|
111
|
+
* the contract.
|
|
112
|
+
*
|
|
113
|
+
* **A unit is delivered only when it is complete.** A lost datagram makes the P2P layer discard the frame
|
|
114
|
+
* it was reassembling, so a unit whose tail never arrives is ended by the next unit's first frame while
|
|
115
|
+
* still full — that one is dropped and reported, never handed on short. Truncated bytes are worse than
|
|
116
|
+
* none: a decoder given an access unit shorter than its own slice headers promise reports bitstream
|
|
117
|
+
* truncation and produces no picture at all.
|
|
118
|
+
*
|
|
119
|
+
* Two consequences worth stating. A unit that is exactly the threshold long, or an exact multiple of it,
|
|
120
|
+
* ends on a full frame and is dropped as truncated — one frame in ~64000, reported, and the alternative is
|
|
121
|
+
* handing a decoder bytes that may genuinely be short. And a station that fills to some OTHER size is not
|
|
122
|
+
* recognised: its units stay exactly as they arrive rather than being mis-joined.
|
|
123
|
+
*/
|
|
124
|
+
export declare class AccessUnitAssembler {
|
|
125
|
+
private readonly onDropped?;
|
|
126
|
+
private open?;
|
|
127
|
+
private droppedUnits;
|
|
128
|
+
/**
|
|
129
|
+
* @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
|
|
130
|
+
* arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
|
|
131
|
+
* its own.
|
|
132
|
+
*/
|
|
133
|
+
constructor(onDropped?: ((drop: {
|
|
134
|
+
carried: number;
|
|
135
|
+
chunks: number;
|
|
136
|
+
count: number;
|
|
137
|
+
}) => void) | undefined);
|
|
138
|
+
/**
|
|
139
|
+
* Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
|
|
140
|
+
* still being filled, one in the ordinary case.
|
|
141
|
+
*
|
|
142
|
+
* `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
|
|
143
|
+
* frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
|
|
144
|
+
* of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
|
|
145
|
+
* carries).
|
|
146
|
+
*/
|
|
147
|
+
push(payload: Buffer, decode: (payload: Buffer) => Buffer | undefined): AssembledAccessUnit[];
|
|
148
|
+
/** Forget an incomplete unit, counting and reporting it. */
|
|
149
|
+
private discard;
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct-binary write helpers.
|
|
3
|
+
*
|
|
4
|
+
* The capability-driven command recipes live in the capability modules
|
|
5
|
+
* (`src/model/capabilities/*`): each module owns its own `buildCommand`, and the barrel's
|
|
6
|
+
* `buildCommand()` resolves across them. This file keeps ONLY the **direct-binary** body builder
|
|
7
|
+
* — a packed struct, not a JSON recipe — used by `EufyMega.sendDirectBinary` for camera on/off +
|
|
8
|
+
* spotlight brightness/color-temp/enable. No dependency on the capability barrel (avoids a cycle).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Build a level-2 direct-binary control body: `[u32 channel?][u32 value][account_id ASCII pad→128]`.
|
|
12
|
+
* Omit `channel` for the channel-less station form (e.g. HomeBase alarm volume). See the struct note above.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDirectBinaryBody(value: number, accountId: string, channel?: number): Buffer;
|
|
15
|
+
/**
|
|
16
|
+
* Body for the device/hub rename command (`SET_DEVICE_NAME` 1217 / `SET_HUB_NAME` 1216). A 261-byte
|
|
17
|
+
* struct confirmed against the app: `[u32 = 0][u8 device_channel][name → 128B, null-padded]
|
|
18
|
+
* [account_id → 128B, null-padded]`. Sent signCode 8 on the station channel (255). The name is UTF-8
|
|
19
|
+
* (matching the app), truncated to fit 127 bytes so the field stays null-terminated.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildDeviceNameBody(channel: number, name: string, accountId: string): Buffer;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FcmCredentials } from "./types.js";
|
|
2
|
+
import { type Logger } from "../../core/logger.js";
|
|
3
|
+
export declare const FCM: {
|
|
4
|
+
readonly PROJECT_ID: "batterycam-3250a";
|
|
5
|
+
readonly API_KEY: "AIzaSyCSz1uxGrHXsEktm7O3_wv-uLGpC9BvXR8";
|
|
6
|
+
readonly APP_ID: "1:348804314802:android:440a6773b3620da7";
|
|
7
|
+
readonly SENDER_ID: "348804314802";
|
|
8
|
+
readonly PACKAGE: "com.oceanwing.battery.cam";
|
|
9
|
+
readonly CERT_SHA1: "F051262F9F99B638F3C76DE349830638555B4A0A";
|
|
10
|
+
readonly AUTH_VERSION: "FIS_v2";
|
|
11
|
+
readonly SDK_VERSION: "a:16.3.1";
|
|
12
|
+
};
|
|
13
|
+
/** Generate a valid Firebase Installation ID (22 url-safe chars, starts c-f). */
|
|
14
|
+
export declare function generateFid(): string;
|
|
15
|
+
export declare class FcmRegistrar {
|
|
16
|
+
private readonly logger;
|
|
17
|
+
constructor(logger?: Logger);
|
|
18
|
+
/** Full registration → FcmCredentials. */
|
|
19
|
+
register(): Promise<FcmCredentials>;
|
|
20
|
+
private installFid;
|
|
21
|
+
private checkin;
|
|
22
|
+
private gcmRegister;
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PushClient } from "./push-client.js";
|
|
2
|
+
export { FcmRegistrar, generateFid, FCM } from "./fcm.js";
|
|
3
|
+
export { McsParser } from "./parser.js";
|
|
4
|
+
export { FileFcmStore, MemoryFcmStore, type FcmStore, type PersistedPush } from "./store.js";
|
|
5
|
+
export { MessageTag } from "./message-tags.js";
|
|
6
|
+
export type { PushEvent, ThumbnailCandidate, ThumbnailCandidateAttribution, PushPayload, PushEnrichment, RawPushMessage, EufyPushMessage, FcmCredentials, McsMessage, } from "./types.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCS wire-framing tags — the 1-byte tag preceding each framed message on Google's MCS/FCM socket.
|
|
3
|
+
*
|
|
4
|
+
* Pure transport framing (login/heartbeat/data-stanza routing); it is NOT a push *event* semantic,
|
|
5
|
+
* so it lives with the push transport, not in the model's push-event vocabulary
|
|
6
|
+
* (the model layer's push-event module). The parser and client are its only consumers.
|
|
7
|
+
*/
|
|
8
|
+
export declare enum MessageTag {
|
|
9
|
+
HeartbeatPing = 0,
|
|
10
|
+
HeartbeatAck = 1,
|
|
11
|
+
LoginRequest = 2,
|
|
12
|
+
LoginResponse = 3,
|
|
13
|
+
Close = 4,
|
|
14
|
+
MessageStanza = 5,
|
|
15
|
+
PresenceStanza = 6,
|
|
16
|
+
IqStanza = 7,
|
|
17
|
+
DataMessageStanza = 8,
|
|
18
|
+
BatchPresenceStanza = 9,
|
|
19
|
+
StreamErrorStanza = 10,
|
|
20
|
+
HttpRequest = 11,
|
|
21
|
+
HttpResponse = 12,
|
|
22
|
+
BindAccountRequest = 13,
|
|
23
|
+
BindAccountResponse = 14,
|
|
24
|
+
TalkMetadata = 15,
|
|
25
|
+
NumProtoTypes = 16
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCS wire-stream parser. The TLS stream is framed as:
|
|
3
|
+
* first packet: [version:1][tag:1][varint length][protobuf]
|
|
4
|
+
* then each msg: [tag:1][varint length][protobuf]
|
|
5
|
+
* Chunks arrive arbitrarily split, so we accumulate and re-enter as data lands.
|
|
6
|
+
* Implements Google's MCS (mtalk) stream framing.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
export declare class McsParser extends EventEmitter {
|
|
10
|
+
private data;
|
|
11
|
+
private state;
|
|
12
|
+
private messageTag;
|
|
13
|
+
private messageSize;
|
|
14
|
+
private sizePacketSoFar;
|
|
15
|
+
/** Reset for a fresh connection. */
|
|
16
|
+
reset(): void;
|
|
17
|
+
/** Feed a chunk of TLS bytes. */
|
|
18
|
+
handleData(chunk: Buffer): void;
|
|
19
|
+
private minBytesNeeded;
|
|
20
|
+
private waitForData;
|
|
21
|
+
private onVersion;
|
|
22
|
+
private onTag;
|
|
23
|
+
private onSize;
|
|
24
|
+
private onBytes;
|
|
25
|
+
private next;
|
|
26
|
+
private emitMessage;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Root } from "protobufjs";
|
|
2
|
+
/**
|
|
3
|
+
* Google MCS + Android check-in protobuf schemas, embedded as strings so they
|
|
4
|
+
* ship in the build without a file-copy step (protobufjs `parse()` instead of
|
|
5
|
+
* `load()`). These are Google's public protocol definitions (Chromium MCS /
|
|
6
|
+
* GServices check-in), not eufy code.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MCS_PROTO = "\nsyntax = \"proto2\";\noption optimize_for = LITE_RUNTIME;\npackage mcs_proto;\n\nmessage HeartbeatPing {\n optional int32 stream_id = 1;\n optional int32 last_stream_id_received = 2;\n optional int64 status = 3;\n}\nmessage HeartbeatAck {\n optional int32 stream_id = 1;\n optional int32 last_stream_id_received = 2;\n optional int64 status = 3;\n}\nmessage ErrorInfo {\n required int32 code = 1;\n optional string message = 2;\n optional string type = 3;\n optional Extension extension = 4;\n}\nmessage Setting {\n required string name = 1;\n required string value = 2;\n}\nmessage HeartbeatStat {\n required string ip = 1;\n required bool timeout = 2;\n required int32 interval_ms = 3;\n}\nmessage HeartbeatConfig {\n optional bool upload_stat = 1;\n optional string ip = 2;\n optional int32 interval_ms = 3;\n}\nmessage ClientEvent {\n enum Type { UNKNOWN = 0; DISCARDED_EVENTS = 1; FAILED_CONNECTION = 2; SUCCESSFUL_CONNECTION = 3; }\n optional Type type = 1;\n optional uint32 number_discarded_events = 100;\n optional int32 network_type = 200;\n reserved 201;\n optional uint64 time_connection_started_ms = 202;\n optional uint64 time_connection_ended_ms = 203;\n optional int32 error_code = 204;\n optional uint64 time_connection_established_ms = 300;\n}\nmessage LoginRequest {\n enum AuthService { ANDROID_ID = 2; }\n required string id = 1;\n required string domain = 2;\n required string user = 3;\n required string resource = 4;\n required string auth_token = 5;\n optional string device_id = 6;\n optional int64 last_rmq_id = 7;\n repeated Setting setting = 8;\n repeated string received_persistent_id = 10;\n optional bool adaptive_heartbeat = 12;\n optional HeartbeatStat heartbeat_stat = 13;\n optional bool use_rmq2 = 14;\n optional int64 account_id = 15;\n optional AuthService auth_service = 16;\n optional int32 network_type = 17;\n optional int64 status = 18;\n reserved 19, 20, 21;\n repeated ClientEvent client_event = 22;\n}\nmessage LoginResponse {\n required string id = 1;\n optional string jid = 2;\n optional ErrorInfo error = 3;\n repeated Setting setting = 4;\n optional int32 stream_id = 5;\n optional int32 last_stream_id_received = 6;\n optional HeartbeatConfig heartbeat_config = 7;\n optional int64 server_timestamp = 8;\n}\nmessage StreamErrorStanza {\n required string type = 1;\n optional string text = 2;\n}\nmessage Close {}\nmessage Extension {\n required int32 id = 1;\n required bytes data = 2;\n}\nmessage IqStanza {\n enum IqType { GET = 0; SET = 1; RESULT = 2; IQ_ERROR = 3; }\n optional int64 rmq_id = 1;\n required IqType type = 2;\n required string id = 3;\n optional string from = 4;\n optional string to = 5;\n optional ErrorInfo error = 6;\n optional Extension extension = 7;\n optional string persistent_id = 8;\n optional int32 stream_id = 9;\n optional int32 last_stream_id_received = 10;\n optional int64 account_id = 11;\n optional int64 status = 12;\n}\nmessage AppData {\n required string key = 1;\n required string value = 2;\n}\nmessage DataMessageStanza {\n optional string id = 2;\n required string from = 3;\n optional string to = 4;\n required string category = 5;\n optional string token = 6;\n repeated AppData app_data = 7;\n optional bool from_trusted_server = 8;\n optional string persistent_id = 9;\n optional int32 stream_id = 10;\n optional int32 last_stream_id_received = 11;\n optional string reg_id = 13;\n optional int64 device_user_id = 16;\n optional int32 ttl = 17;\n optional int64 sent = 18;\n optional int32 queued = 19;\n optional int64 status = 20;\n optional bytes raw_data = 21;\n optional bool immediate_ack = 24;\n}\nmessage StreamAck {}\nmessage SelectiveAck { repeated string id = 1; }\n";
|
|
9
|
+
export declare const CHECKIN_PROTO = "\nsyntax = \"proto2\";\noption optimize_for = LITE_RUNTIME;\n\nmessage CheckinRequest {\n optional string imei = 1;\n optional int64 androidId = 2;\n optional string digest = 3;\n required Checkin checkin = 4;\n message Checkin {\n required Build build = 1;\n message Build {\n optional string fingerprint = 1;\n optional string hardware = 2;\n optional string brand = 3;\n optional string radio = 4;\n optional string bootloader = 5;\n optional string clientId = 6;\n optional int64 time = 7;\n optional int32 packageVersionCode = 8;\n optional string device = 9;\n optional int32 sdkVersion = 10;\n optional string model = 11;\n optional string manufacturer = 12;\n optional string product = 13;\n optional bool otaInstalled = 14;\n }\n optional int64 lastCheckinMs = 2;\n repeated Event event = 3;\n message Event {\n optional string tag = 1;\n optional string value = 2;\n optional int64 timeMs = 3;\n }\n repeated Statistic stat = 4;\n message Statistic {\n required string tag = 1;\n optional int32 count = 2;\n optional float sum = 3;\n }\n repeated string requestedGroup = 5;\n optional string cellOperator = 6;\n optional string simOperator = 7;\n optional string roaming = 8;\n optional int32 userNumber = 9;\n }\n optional string desiredBuild = 5;\n optional string locale = 6;\n optional int64 loggingId = 7;\n optional string marketCheckin = 8;\n repeated string macAddress = 9;\n optional string meid = 10;\n repeated string accountCookie = 11;\n optional string timeZone = 12;\n optional fixed64 securityToken = 13;\n optional int32 version = 14;\n repeated string otaCert = 15;\n optional string serial = 16;\n optional string esn = 17;\n repeated string macAddressType = 19;\n required int32 fragment = 20;\n optional string userName = 21;\n optional int32 userSerialNumber = 22;\n}\n\nmessage CheckinResponse {\n optional bool statsOk = 1;\n repeated Intent intent = 2;\n message Intent {\n optional string action = 1;\n optional string dataUri = 2;\n optional string mimeType = 3;\n optional string javaClass = 4;\n repeated Extra extra = 5;\n message Extra {\n optional string name = 6;\n optional string value = 7;\n }\n }\n optional int64 timeMs = 3;\n optional string digest = 4;\n repeated GservicesSetting setting = 5;\n message GservicesSetting {\n optional bytes name = 1;\n optional bytes value = 2;\n }\n optional bool marketOk = 6;\n optional fixed64 androidId = 7;\n optional fixed64 securityToken = 8;\n optional bool settingsDiff = 9;\n repeated string deleteSetting = 10;\n optional string versionInfo = 11;\n optional string deviceDataVersionInfo = 12;\n}\n";
|
|
10
|
+
export declare const mcsRoot: () => Root;
|
|
11
|
+
export declare const checkinRoot: () => Root;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FCM/MCS push client — holds a persistent TLS connection to Google's MCS
|
|
3
|
+
* (mtalk.google.com:5228), logs in with the check-in androidId/securityToken,
|
|
4
|
+
* heartbeats, and decodes DataMessageStanza pushes into eufy PushEvents.
|
|
5
|
+
*
|
|
6
|
+
* Implements Google's FCM/MCS push protocol; live-verified against real account pushes.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import type { FcmCredentials, PushEvent, RawPushMessage } from "./types.js";
|
|
10
|
+
import { type Logger } from "../../core/logger.js";
|
|
11
|
+
/**
|
|
12
|
+
* Normalises a decoded eufy envelope without consulting device semantics; semantic event names remain unset.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizePushEvent(raw: RawPushMessage): PushEvent;
|
|
16
|
+
export declare class PushClient extends EventEmitter {
|
|
17
|
+
private readonly creds;
|
|
18
|
+
private readonly logger;
|
|
19
|
+
/** Consecutive MCS login rejections tolerated (self-healing propagation) before surfacing an error. */
|
|
20
|
+
private static readonly MAX_LOGIN_FAILURES;
|
|
21
|
+
private socket?;
|
|
22
|
+
private readonly parser;
|
|
23
|
+
private heartbeatTimer?;
|
|
24
|
+
private reconnectTimer?;
|
|
25
|
+
private currentDelay;
|
|
26
|
+
private persistentIds;
|
|
27
|
+
private loggedIn;
|
|
28
|
+
private closing;
|
|
29
|
+
/** Consecutive MCS login rejections — transient ones self-heal via reconnect (see {@link onMessage}). */
|
|
30
|
+
private loginFailures;
|
|
31
|
+
constructor(creds: FcmCredentials, logger?: Logger);
|
|
32
|
+
/** Persistent ids already seen (set this from storage to avoid re-delivery). */
|
|
33
|
+
setPersistentIds(ids: string[]): void;
|
|
34
|
+
getPersistentIds(): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Open the MCS connection and log in.
|
|
37
|
+
*
|
|
38
|
+
* `servername` is passed explicitly: Node sends SNI only when told to, never deriving it from `host`,
|
|
39
|
+
* and this endpoint answers a connection without SNI with a self-signed certificate naming
|
|
40
|
+
* `invalid2.invalid` — which now fails the handshake rather than being accepted. Verification matters
|
|
41
|
+
* because the login request carries the account's `securityToken`, and an unverified peer could both
|
|
42
|
+
* read it and inject forged pushes into the event path.
|
|
43
|
+
*/
|
|
44
|
+
connect(): void;
|
|
45
|
+
private buildLoginRequest;
|
|
46
|
+
private buildHeartbeatPing;
|
|
47
|
+
private buildHeartbeatAck;
|
|
48
|
+
private onMessage;
|
|
49
|
+
/**
|
|
50
|
+
* Handle an MCS `LoginResponse` carrying an error. Google occasionally rejects the FIRST login right
|
|
51
|
+
* after check-in (`wrong_secret`) while the freshly-registered androidId/securityToken propagates —
|
|
52
|
+
* it succeeds on the very next attempt. So a login rejection is treated as **transient**: log it and
|
|
53
|
+
* close the socket to let the existing backoff reconnect retry with the same creds, rather than
|
|
54
|
+
* surfacing a self-healing blip as a host-facing `error`. Only once it persists past
|
|
55
|
+
* {@link MAX_LOGIN_FAILURES} consecutive attempts (creds genuinely stale) is it emitted as `error`.
|
|
56
|
+
*/
|
|
57
|
+
private onLoginError;
|
|
58
|
+
private handleDataMessage;
|
|
59
|
+
private startHeartbeat;
|
|
60
|
+
private stopHeartbeat;
|
|
61
|
+
private onClose;
|
|
62
|
+
private scheduleReconnect;
|
|
63
|
+
close(): void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FcmCredentials } from "./types.js";
|
|
2
|
+
export interface PersistedPush {
|
|
3
|
+
creds: FcmCredentials;
|
|
4
|
+
persistentIds: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface FcmStore {
|
|
7
|
+
load(): PersistedPush | null;
|
|
8
|
+
save(p: PersistedPush): void;
|
|
9
|
+
clear(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class MemoryFcmStore implements FcmStore {
|
|
12
|
+
private p;
|
|
13
|
+
load(): PersistedPush | null;
|
|
14
|
+
save(p: PersistedPush): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare class FileFcmStore implements FcmStore {
|
|
18
|
+
private readonly path;
|
|
19
|
+
constructor(path: string);
|
|
20
|
+
load(): PersistedPush | null;
|
|
21
|
+
save(p: PersistedPush): void;
|
|
22
|
+
clear(): void;
|
|
23
|
+
}
|