@mega-yfue/eufy-sdk 0.2.0-beta.1 → 0.2.0-beta.11
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/dist/client/eufy-mega.d.ts +16 -11
- package/dist/core/contracts.d.ts +57 -27
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/logger.d.ts +5 -3
- package/dist/core/solix-types.d.ts +36 -0
- package/dist/core/store.d.ts +20 -9
- package/dist/index.js +1446 -192
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +58 -28
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/index.d.ts +11 -5
- package/dist/model/capabilities/solix.d.ts +75 -0
- package/dist/model/capabilities/types.d.ts +16 -4
- package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
- package/dist/model/index.d.ts +3 -0
- package/dist/model/param-dictionary.d.ts +24 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/solix-catalog.d.ts +20 -0
- package/dist/model/solix-device.d.ts +102 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/solix-client.d.ts +158 -0
- package/dist/transport/http/solix-constants.d.ts +29 -0
- package/dist/transport/mqtt/index.d.ts +2 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +214 -0
- package/dist/transport/mqtt/topics.d.ts +20 -0
- package/dist/transport/p2p/command-router.d.ts +143 -15
- package/dist/transport/p2p/index.d.ts +1 -0
- package/dist/transport/p2p/live-stream.d.ts +5 -4
- package/dist/transport/p2p/live-trace.d.ts +100 -6
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +4 -0
- package/dist/transport/p2p/session-manager.d.ts +57 -23
- package/dist/transport/p2p/shared-live-source.d.ts +10 -1
- package/package.json +3 -2
|
@@ -18,6 +18,23 @@ import type { FfmpegLevel } from "../ffmpeg.js";
|
|
|
18
18
|
import { SharedLiveSource } from "./shared-live-source.js";
|
|
19
19
|
import { type PowerTier, type SessionManagerOpts } from "./session-manager.js";
|
|
20
20
|
import { FragmentRecording } from "./fragment-recording.js";
|
|
21
|
+
/**
|
|
22
|
+
* What a caller's own deadline on a station call has to clear, in milliseconds.
|
|
23
|
+
*
|
|
24
|
+
* A caller that bounds one of these calls itself races these waits, and a bound below them reports the
|
|
25
|
+
* caller's own expiry in place of the reason this SDK was about to give — the two are indistinguishable to
|
|
26
|
+
* whoever reads the outcome, and they call for different next steps. Published so that bound can be derived
|
|
27
|
+
* rather than copied: a literal in a caller's source is a second source of truth that goes stale silently
|
|
28
|
+
* when these change.
|
|
29
|
+
*
|
|
30
|
+
* `connect` applies to every call on a station, because nothing can be addressed to one before its session is
|
|
31
|
+
* up. `level2Grace` applies twice where the key is required: the negotiation is re-prompted once.
|
|
32
|
+
*/
|
|
33
|
+
export declare const P2P_STATION_WAITS: {
|
|
34
|
+
readonly connect: 20000;
|
|
35
|
+
readonly level2Grace: 25000;
|
|
36
|
+
readonly level2Settle: 8000;
|
|
37
|
+
};
|
|
21
38
|
/**
|
|
22
39
|
* Options accepted when warming a {@link SharedLiveSource} for a device (all optional).
|
|
23
40
|
*
|
|
@@ -71,7 +88,7 @@ export interface P2PRouterDeps {
|
|
|
71
88
|
}
|
|
72
89
|
export declare class P2PCommandRouter {
|
|
73
90
|
private readonly deps;
|
|
74
|
-
/**
|
|
91
|
+
/** P2P session lifecycle: on-demand open + battery-aware idle-detach + refcount, per session key. */
|
|
75
92
|
private readonly manager;
|
|
76
93
|
/** Error objects already forwarded while a station startup awaits the same session signal. */
|
|
77
94
|
private readonly reportedErrors;
|
|
@@ -79,6 +96,21 @@ export declare class P2PCommandRouter {
|
|
|
79
96
|
private readonly liveSources;
|
|
80
97
|
/** The options each live source was built from, so a later caller's conflicting ones can be reported. */
|
|
81
98
|
private readonly liveSourceOpts;
|
|
99
|
+
/**
|
|
100
|
+
* The session each live source pulls over — the station's own serial, or the source's own media
|
|
101
|
+
* session key.
|
|
102
|
+
*
|
|
103
|
+
* Written the instant the choice is made and BEFORE the connection is opened, which is what makes it
|
|
104
|
+
* a reservation rather than a record. Two cameras started together (the four-tile case this feature
|
|
105
|
+
* exists for) would otherwise both find the station's session free: a consumer attaches only after
|
|
106
|
+
* `sharedLiveSourceFor` returns, so neither is visible to the other through consumer counts, and both
|
|
107
|
+
* would take the shared connection and contend on it.
|
|
108
|
+
*
|
|
109
|
+
* A station entry is kept as well as a media one, because "is the station's own session already
|
|
110
|
+
* claimed" is the question being asked, and an entry that is present but not yet in
|
|
111
|
+
* {@link liveSources} is a claim in flight.
|
|
112
|
+
*/
|
|
113
|
+
private readonly liveSessionKeys;
|
|
82
114
|
/**
|
|
83
115
|
* The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
|
|
84
116
|
* time and the session carries one audio sequence, so this path is exclusive where a live pull is
|
|
@@ -90,6 +122,12 @@ export declare class P2PCommandRouter {
|
|
|
90
122
|
constructor(deps: P2PRouterDeps);
|
|
91
123
|
/** Forward one P2P failure once even when both the session listener and startup waiter observe it. */
|
|
92
124
|
private reportError;
|
|
125
|
+
/**
|
|
126
|
+
* Emit a live trace under a station session's handle, for work this router does ON that session before
|
|
127
|
+
* the session itself records anything — reaching the station, and resolving what a device is on it. Same
|
|
128
|
+
* handle as everything the session goes on to trace, which is what groups one attempt.
|
|
129
|
+
*/
|
|
130
|
+
private traceOnStation;
|
|
93
131
|
/**
|
|
94
132
|
* Whether this transport stack drives `dev`'s `ff09-*` commands — true when the device has its own
|
|
95
133
|
* usable P2P endpoint (a non-empty `p2p_did`). The command sink asks each stack this to route a
|
|
@@ -99,7 +137,10 @@ export declare class P2PCommandRouter {
|
|
|
99
137
|
* `no P2P session`.
|
|
100
138
|
*/
|
|
101
139
|
static claimsDevice(dev: EufyDevice): boolean;
|
|
102
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* The open P2P sessions by key — a station's own under its serial, a camera's media session under
|
|
142
|
+
* `<stationSn>#live:<channel>` (a snapshot; mutate via the lifecycle methods, not this map).
|
|
143
|
+
*/
|
|
103
144
|
getSessions(): Map<string, P2PSession>;
|
|
104
145
|
/**
|
|
105
146
|
* Speculatively open + briefly hold a station's session (e.g. after a doorbell ring) so a
|
|
@@ -109,7 +150,7 @@ export declare class P2PCommandRouter {
|
|
|
109
150
|
*
|
|
110
151
|
* One hold, taken before the open so a slow connect can't idle-close mid-flight. It expires on its
|
|
111
152
|
* own, which arms the station's idle window rather than closing the session, per {@link PREWARM_MS}.
|
|
112
|
-
* A second hold after the open would buy nothing: {@link
|
|
153
|
+
* A second hold after the open would buy nothing: {@link openSession} returns once the socket is bound
|
|
113
154
|
* and the lookups are away, not once the peer has answered, so both would expire together.
|
|
114
155
|
*
|
|
115
156
|
* Best-effort — a failed open surfaces via `onError`. A {@link SessionSupersededError} does not: the
|
|
@@ -142,7 +183,41 @@ export declare class P2PCommandRouter {
|
|
|
142
183
|
* when present, else the freshest private IP in the record ({@link freshestLanIp}) — so P2P works
|
|
143
184
|
* on-LAN even when broadcast is blocked (AP isolation) or the record's `ip_addr` went stale.
|
|
144
185
|
*/
|
|
145
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The key a camera's own media session is filed under, distinct from every station serial because a
|
|
188
|
+
* serial contains no `#`.
|
|
189
|
+
*/
|
|
190
|
+
private static mediaSessionKey;
|
|
191
|
+
/** Whether `key` names a media session rather than a station's own. */
|
|
192
|
+
private static isMediaSessionKey;
|
|
193
|
+
/**
|
|
194
|
+
* Open (or reuse) a SECOND connection to a station, carrying one camera's media and nothing else.
|
|
195
|
+
*
|
|
196
|
+
* One session serves one camera: a station fans its cameras over a session and answers the most recent
|
|
197
|
+
* start on it, so two cameras down one tunnel take it from each other in turn. Another connection is
|
|
198
|
+
* how a station serves another camera.
|
|
199
|
+
*
|
|
200
|
+
* The hardware was shown to do this before the SDK did. A first-party display showing four tiles was
|
|
201
|
+
* captured opening one PPCS session per camera, with three cameras' video arriving in the same second
|
|
202
|
+
* at 2304x1296, 1600x1200 and 3840x2160 — three geometries at once, which no composed stream can be.
|
|
203
|
+
* Reproduced here afterwards on a base carrying two attached cameras, one at 3840x2160, both holding
|
|
204
|
+
* full frame rate at once over a session each, where the same pair down one session could only take
|
|
205
|
+
* turns.
|
|
206
|
+
*
|
|
207
|
+
* It carries media alone. The station announces its state to every client that connects, so a session
|
|
208
|
+
* wired to the same fan-out would report every event a second time; {@link makeSession} leaves this one
|
|
209
|
+
* unannounced, and the station's own session stays the single source of connection state, control
|
|
210
|
+
* notifications and frames.
|
|
211
|
+
*
|
|
212
|
+
* The level-2 key is waited for here on the same terms the station's own session gets, because a
|
|
213
|
+
* connection is not usable for this without one: an attached camera's media start has no level-1 form,
|
|
214
|
+
* so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
|
|
215
|
+
* shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
|
|
216
|
+
* carry the start is not a connected session, so this refuses rather than returning one.
|
|
217
|
+
*/
|
|
218
|
+
private openMediaSession;
|
|
219
|
+
/** Open (or reuse) the session filed under `key`, dialling `parentSn`'s endpoint. */
|
|
220
|
+
private openSession;
|
|
146
221
|
/**
|
|
147
222
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
148
223
|
*
|
|
@@ -154,6 +229,12 @@ export declare class P2PCommandRouter {
|
|
|
154
229
|
*
|
|
155
230
|
* The `close` handler drops the session from the {@link SessionManager} and disposes any shared live
|
|
156
231
|
* source riding this station (consumers get `stop`; a later attach rebuilds via the factory).
|
|
232
|
+
*
|
|
233
|
+
* A session filed under a media key is wired for errors and its own teardown ONLY. Connection state,
|
|
234
|
+
* level-2 readiness and inbound frames all reach the owner through the station's own session, and a
|
|
235
|
+
* station announces those to every client that connects — so fanning a second connection's copies out
|
|
236
|
+
* under the same station serial would report each one twice, and a close would tear down the station
|
|
237
|
+
* while its own session is still serving.
|
|
157
238
|
*/
|
|
158
239
|
private makeSession;
|
|
159
240
|
/**
|
|
@@ -262,22 +343,40 @@ export declare class P2PCommandRouter {
|
|
|
262
343
|
* {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
|
|
263
344
|
* reuse branch, so a reuse frees the station as a cold start does.
|
|
264
345
|
*
|
|
346
|
+
* `mayOpenOwnSession` decides whether a camera that finds the station's own session claimed may open a
|
|
347
|
+
* connection of its own for it. Only the continuous-pull egresses pass it. A still must not: it wants one
|
|
348
|
+
* frame, and a socket plus a level-2 negotiation per thumbnail is a cost a tile refresh cannot justify —
|
|
349
|
+
* so a still asked for while a sibling is being watched contends as it always did, and the caller's
|
|
350
|
+
* retained image answers it. A live view still outranks a tile; what changed is that two live views no
|
|
351
|
+
* longer have to outrank each other.
|
|
352
|
+
*
|
|
265
353
|
* The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
|
|
266
354
|
* place.
|
|
267
355
|
*/
|
|
268
|
-
sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts): Promise<SharedLiveSource>;
|
|
356
|
+
sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, mayOpenOwnSession?: boolean): Promise<SharedLiveSource>;
|
|
269
357
|
/**
|
|
270
358
|
* Tear down any pull on this station that is lingering for ANOTHER camera, before starting this one.
|
|
271
359
|
*
|
|
272
360
|
* A lingering pull has no consumers but is still held open, and on an attached camera holding it open means
|
|
273
|
-
* re-sending the full media start every keepalive tick. Two channels doing that at once
|
|
274
|
-
* serves one camera at a time leaves the new stream receiving nothing but the old camera's frames
|
|
275
|
-
* long as the linger lasts.
|
|
361
|
+
* re-sending the full media start every keepalive tick. Two channels doing that at once over ONE session,
|
|
362
|
+
* which serves one camera at a time, leaves the new stream receiving nothing but the old camera's frames
|
|
363
|
+
* for as long as the linger lasts.
|
|
276
364
|
*
|
|
277
365
|
* Several cameras genuinely being WATCHED together are never disturbed — the linger exists to make
|
|
278
366
|
* re-opening the SAME camera cheap, and it keeps doing that. What it may not do is keep a camera nobody is
|
|
279
367
|
* looking at competing with one somebody just asked for.
|
|
280
368
|
*
|
|
369
|
+
* An `idle` source is skipped, because it is not a linger and holds nothing: it has never warmed, so it
|
|
370
|
+
* has sent no media start and is competing for nothing. Without that, two cameras opened at the same
|
|
371
|
+
* moment destroy each other — the second finds the first's source built but not yet attached to, reads
|
|
372
|
+
* zero consumers as a linger, and disposes the source its caller is holding. Which is the four-tile case
|
|
373
|
+
* this whole path exists for.
|
|
374
|
+
*
|
|
375
|
+
* A sibling lingering on a connection of its OWN is skipped for the same reason stated the other way: the
|
|
376
|
+
* contention this releases is contention over one session, and that sibling is not on this one. Dropping
|
|
377
|
+
* it would close a socket and throw away the cheap re-attach the linger exists to provide, to relieve a
|
|
378
|
+
* competition that is not happening.
|
|
379
|
+
*
|
|
281
380
|
* A snapshot tile is nobody looking. Opening a live view in the Home app takes that cell fullscreen, so the
|
|
282
381
|
* pulls refreshing the other cells are off screen, yet each goes on re-issuing its own media start every
|
|
283
382
|
* retry tick — measured as four pulls warming together off one HomeBase, a live request landing 1.4 s later,
|
|
@@ -288,14 +387,27 @@ export declare class P2PCommandRouter {
|
|
|
288
387
|
*/
|
|
289
388
|
private releaseLingeringSiblings;
|
|
290
389
|
/**
|
|
291
|
-
*
|
|
390
|
+
* Whether another camera has already claimed this station's OWN session, ignoring `key`.
|
|
391
|
+
*
|
|
392
|
+
* The question a newcomer has to answer is not whether the station is busy — it can serve one camera
|
|
393
|
+
* per connection — but whether the connection it would otherwise share is taken. A camera on a media
|
|
394
|
+
* session of its own does not hold this one, so a station whose first camera has since stopped hands
|
|
395
|
+
* its own session to the next arrival rather than opening a socket beside an idle one.
|
|
396
|
+
*
|
|
397
|
+
* A claim counts while its source is still `idle`, source or no source. That is a start that has been
|
|
398
|
+
* handed to its caller but not yet attached to, and it is the only state in which two cameras asked for
|
|
399
|
+
* at the same moment can see each other: consumers attach after this method has already run for both.
|
|
400
|
+
*
|
|
401
|
+
* The cost is that a pull genuinely abandoned before its first attach goes on holding the station's own
|
|
402
|
+
* session, and the next camera pays for a connection of its own rather than reclaiming it. That is one
|
|
403
|
+
* socket against destroying a start someone is waiting on, which is not a close trade.
|
|
292
404
|
*
|
|
293
405
|
* A stopped source is skipped even when consumers are still attached to it. A failed start fails its
|
|
294
406
|
* consumers without detaching them, so a caller still holding a dead handle leaves the count non-zero,
|
|
295
|
-
* and counting that as a viewer would
|
|
296
|
-
* restarted. Only a source that can still deliver holds a place.
|
|
407
|
+
* and counting that as a viewer would put every later camera on a connection of its own until the
|
|
408
|
+
* client restarted. Only a source that can still deliver holds a place.
|
|
297
409
|
*/
|
|
298
|
-
private
|
|
410
|
+
private stationSessionInUse;
|
|
299
411
|
/**
|
|
300
412
|
* Whether the stream on `key` should re-assert its channel to hold the station.
|
|
301
413
|
*
|
|
@@ -305,8 +417,8 @@ export declare class P2PCommandRouter {
|
|
|
305
417
|
* - Nothing attached: no. There is nobody to take the station for.
|
|
306
418
|
* - A live viewer attached: yes. That is the picture someone is looking at.
|
|
307
419
|
* - Held only for stills, while a sibling on this station has a live viewer: no. A still refreshes a
|
|
308
|
-
* tile that is off screen while the live view is on it, and
|
|
309
|
-
* cannot satisfy both. Measured: a still on a sibling halved a live view's frame rate for as long as
|
|
420
|
+
* tile that is off screen while the live view is on it, and one session serving one camera at a time
|
|
421
|
+
* cannot satisfy both — and a still does not open a connection of its own. Measured: a still on a sibling halved a live view's frame rate for as long as
|
|
310
422
|
* it took, and its own capture then took fifteen seconds because it was contending.
|
|
311
423
|
*
|
|
312
424
|
* A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
|
|
@@ -321,8 +433,24 @@ export declare class P2PCommandRouter {
|
|
|
321
433
|
* not.
|
|
322
434
|
*/
|
|
323
435
|
private attachUnlessAborted;
|
|
324
|
-
/**
|
|
436
|
+
/**
|
|
437
|
+
* Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
|
|
438
|
+
* on a session goes with it, and a media session opened for this source alone is closed: nothing else
|
|
439
|
+
* can reach that connection, so leaving it open would hold a socket and a station keepalive for a
|
|
440
|
+
* camera no longer being pulled. A claim on the STATION's own session is released without closing
|
|
441
|
+
* anything — that connection carries the station's control traffic and outlives any one camera.
|
|
442
|
+
*/
|
|
325
443
|
private dropLiveSource;
|
|
444
|
+
/** Close and forget the media session `key`'s live source owned, if it owned one. */
|
|
445
|
+
private closeMediaSession;
|
|
446
|
+
/**
|
|
447
|
+
* Drop the live source a media session was carrying, after that session closed on its own.
|
|
448
|
+
*
|
|
449
|
+
* The source holds the closed connection and never re-resolves it, so it can only answer its retained
|
|
450
|
+
* keyframe and then fail on its warm-up deadline. Its consumers get `stop`, and the next attach builds
|
|
451
|
+
* a fresh source — which, finding the station busy again, opens a fresh media session for it.
|
|
452
|
+
*/
|
|
453
|
+
private tearDownMediaSession;
|
|
326
454
|
/**
|
|
327
455
|
* Drop everything that was riding a station's session, and report the station closed.
|
|
328
456
|
*
|
|
@@ -10,4 +10,5 @@ export * from "./envelope.js";
|
|
|
10
10
|
export * from "./write-commands.js";
|
|
11
11
|
export * from "./lan-ip.js";
|
|
12
12
|
export { LIVE_TRACE_MESSAGE, type LiveTrace } from "./live-trace.js";
|
|
13
|
+
export { P2P_STATION_WAITS } from "./command-router.js";
|
|
13
14
|
export * as p2pCodec from "./codec.js";
|
|
@@ -67,7 +67,7 @@ export interface LiveStreamOptions {
|
|
|
67
67
|
* How long an attached stream tolerates silence on its own channel before re-asserting again, in ms.
|
|
68
68
|
*
|
|
69
69
|
* The re-assert is settled by the first own-channel frame, because settling it is what stops two attached
|
|
70
|
-
* streams contending
|
|
70
|
+
* streams contending over one session, which serves one camera at a time. Silence for this long says the station
|
|
71
71
|
* is no longer serving this camera, which is the only condition the re-assert was for. Defaults to twice
|
|
72
72
|
* the keepalive interval, so a stream whose media flows never reaches it.
|
|
73
73
|
*/
|
|
@@ -129,8 +129,9 @@ export declare class LiveStream extends EventEmitter {
|
|
|
129
129
|
* Stop re-issuing the media start once this camera's own media has arrived, on an attached camera.
|
|
130
130
|
*
|
|
131
131
|
* The nudge differs by topology and only one branch is a ping: an own-session camera sends a small
|
|
132
|
-
* keepalive, while an attached camera has no such state and re-sends the FULL media start.
|
|
133
|
-
* serves one camera at a time that restart re-asserts this channel against whatever else is warm
|
|
132
|
+
* keepalive, while an attached camera has no such state and re-sends the FULL media start. Over one session,
|
|
133
|
+
* which serves one camera at a time, that restart re-asserts this channel against whatever else is warm on
|
|
134
|
+
* it, so two
|
|
134
135
|
* attached streams restart every interval and contend for the station continuously — measured on a real base
|
|
135
136
|
* as a full start every 3 s from each.
|
|
136
137
|
*
|
|
@@ -185,7 +186,7 @@ export declare class LiveStream extends EventEmitter {
|
|
|
185
186
|
*
|
|
186
187
|
* The match is UNCONDITIONAL, however long a station serves another camera instead of this one.
|
|
187
188
|
*
|
|
188
|
-
*
|
|
189
|
+
* One session serving one camera at a time hands a newly opened camera nothing but its sibling's frames until
|
|
189
190
|
* it switches, so "no media of my own yet, plenty for someone else" is what an ordinary handover looks like
|
|
190
191
|
* and does not distinguish a station that tags differently from one that is simply busy.
|
|
191
192
|
*
|
|
@@ -63,12 +63,37 @@ export type LiveTrace =
|
|
|
63
63
|
phase: "sequence-restart";
|
|
64
64
|
dataType: number;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Work on a station is holding for its session to connect, with the milliseconds it will wait.
|
|
68
|
+
*
|
|
69
|
+
* The earliest phase there is: nothing else on a station can be attempted until its session is up, and a
|
|
70
|
+
* caller whose own deadline expires inside this wait has this record and no other. Emitted only where a wait
|
|
71
|
+
* actually happens, so its absence states that the session was already connected.
|
|
72
|
+
*/
|
|
73
|
+
| {
|
|
74
|
+
phase: "session-connect-wait";
|
|
75
|
+
waitMs: number;
|
|
76
|
+
}
|
|
77
|
+
/** The session connected, after this long. */
|
|
78
|
+
| {
|
|
79
|
+
phase: "session-connected";
|
|
80
|
+
waitedMs: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The session did not connect within its wait, so nothing on this station can be attempted.
|
|
84
|
+
*
|
|
85
|
+
* The one outcome that is otherwise indistinguishable from a station that answered and then refused: both
|
|
86
|
+
* leave a caller with no media and no phase naming a station.
|
|
87
|
+
*/
|
|
88
|
+
| {
|
|
89
|
+
phase: "session-unreachable";
|
|
90
|
+
waitedMs: number;
|
|
91
|
+
}
|
|
66
92
|
/**
|
|
67
93
|
* A live start is holding for the station's level-2 key, with the milliseconds it will wait.
|
|
68
94
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* re-issued — and only these separate them.
|
|
95
|
+
* A start that looks slow is either waiting here, waiting for its session to connect, waiting for the station
|
|
96
|
+
* to serve the channel it was asked for, or being re-issued — and only these phases separate them.
|
|
72
97
|
*/
|
|
73
98
|
| {
|
|
74
99
|
phase: "level2-wait";
|
|
@@ -79,10 +104,63 @@ export type LiveTrace =
|
|
|
79
104
|
phase: "level2-ready";
|
|
80
105
|
cipherId: number;
|
|
81
106
|
}
|
|
82
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* The station's key is not coming, why, and the cipher where a station named one.
|
|
109
|
+
*
|
|
110
|
+
* Every ending of a level-2 wait carries one of these reasons, so a start refused for want of a key is
|
|
111
|
+
* accounted for however it ended. `grace-elapsed` is a wait that ran out and states how long was waited;
|
|
112
|
+
* the rest are answered without waiting, because the negotiation is one-shot per connection and a
|
|
113
|
+
* concluded one is final. `no-cipher-key` and `derivation-failed` are about this account's cipher
|
|
114
|
+
* material, `not-negotiating` and `session-closed` about the station or its connection — and only a
|
|
115
|
+
* reason reached under a negotiation has a cipher to name.
|
|
116
|
+
*
|
|
117
|
+
* A `grace-elapsed` start proceeds at level 1 where it has such a form, and not at all where it does not.
|
|
118
|
+
*/
|
|
83
119
|
| {
|
|
84
|
-
phase: "level2-
|
|
85
|
-
|
|
120
|
+
phase: "level2-unavailable";
|
|
121
|
+
reason: "no-cipher-key" | "derivation-failed" | "not-negotiating" | "session-closed" | "grace-elapsed";
|
|
122
|
+
cipherId?: number;
|
|
123
|
+
waitedMs?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A station's cipher was answered with material for a DIFFERENT cipher, which was used in its place.
|
|
127
|
+
*
|
|
128
|
+
* The one lookup outcome no other phase accounts for: material for the cipher the station named is followed
|
|
129
|
+
* by `level2-ready` or by `level2-unavailable` with `derivation-failed`, an answer holding none by
|
|
130
|
+
* `no-cipher-key`, and a lookup that threw is reported as an error. Substituted material derives to
|
|
131
|
+
* nothing and otherwise reads as a station fault. `cipherId` is the cipher the station asked for,
|
|
132
|
+
* `answeredCipherId` the one whose material was used.
|
|
133
|
+
*/
|
|
134
|
+
| {
|
|
135
|
+
phase: "cipher-fallback";
|
|
136
|
+
cipherId: number;
|
|
137
|
+
answeredCipherId: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The station answered its gateway-info prompt, so a key derivation has begun under the cipher it named.
|
|
141
|
+
*
|
|
142
|
+
* What separates a station that never answered the prompt from one that answered and produced no usable key:
|
|
143
|
+
* without it, `level2-unavailable` with `not-negotiating` covers both, and they are a station or network
|
|
144
|
+
* problem and an account cipher-material problem respectively.
|
|
145
|
+
*/
|
|
146
|
+
| {
|
|
147
|
+
phase: "level2-negotiating";
|
|
148
|
+
cipherId: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A station was resolved for a call, stating what the caller's device is on it and whose station it is.
|
|
152
|
+
*
|
|
153
|
+
* Emitted before anything is sent, so it is the only account of the intended topology on a call that fails
|
|
154
|
+
* during resolution: an attached camera's media start has no unencrypted form, so whether a device was taken
|
|
155
|
+
* as attached decides what its failure means. `stationAdmin` states whether the signed-in account is the
|
|
156
|
+
* station's administrator, which is what a key the account cannot resolve turns on; `unstated` is a device
|
|
157
|
+
* record that names no administrator, which is not the same as naming another.
|
|
158
|
+
*/
|
|
159
|
+
| {
|
|
160
|
+
phase: "station-resolved";
|
|
161
|
+
topology: "attached" | "own";
|
|
162
|
+
channel: number;
|
|
163
|
+
stationAdmin: "self" | "other" | "unstated";
|
|
86
164
|
}
|
|
87
165
|
/** A shared source began warming, with the interval it re-issues on and the deadline it fails at. */
|
|
88
166
|
| {
|
|
@@ -110,6 +188,22 @@ export type LiveTrace =
|
|
|
110
188
|
| {
|
|
111
189
|
phase: "path-stale";
|
|
112
190
|
silentMs: number;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A stream received nothing on its own channel for the stall window, and what was done about it.
|
|
194
|
+
*
|
|
195
|
+
* A station that switches to a sibling leaves the stream it was serving with no frames, no error and no
|
|
196
|
+
* stop, so this silence is the only statement that it happened. `reasserted` re-issued the media start,
|
|
197
|
+
* which is the repair; `declined` left the channel alone because nothing is attached to this pull and
|
|
198
|
+
* taking the station back would take it from a camera someone is watching.
|
|
199
|
+
*
|
|
200
|
+
* Media still arriving means this never fires, so a picture that stopped advancing while this is silent
|
|
201
|
+
* stopped for a reason upstream of the station's attention.
|
|
202
|
+
*/
|
|
203
|
+
| {
|
|
204
|
+
phase: "channel-silent";
|
|
205
|
+
silentMs: number;
|
|
206
|
+
outcome: "reasserted" | "declined";
|
|
113
207
|
};
|
|
114
208
|
/**
|
|
115
209
|
* Record one startup observation at debug level.
|
|
@@ -34,11 +34,22 @@ export declare function openLiveStream(session: P2PSession, opts?: LiveStreamOpt
|
|
|
34
34
|
* The header states the stream's geometry when the capture started, and a stream that reconfigures
|
|
35
35
|
* mid-burst leaves it describing something the returned bytes contradict; the return value describes an
|
|
36
36
|
* image, so the image is its source of truth.
|
|
37
|
+
*
|
|
38
|
+
* The consumer is detached the moment the collected run is complete, and the decode that follows holds no
|
|
39
|
+
* station: it works on bytes already in memory. One session serves one camera at a time and a still never
|
|
40
|
+
* opens a second one, so a still that kept its pull attached across its own decode would deny
|
|
41
|
+
* that station to every live request for the length of an FFmpeg run — measured on a real base as a live
|
|
42
|
+
* request refused 370ms after the still it was waiting on had already collected everything it needed.
|
|
43
|
+
*
|
|
44
|
+
* `signal` ends the collection itself, not only the wait for it, and rejects with the signal's own reason
|
|
45
|
+
* because the abandonment is the caller's fact and not a failure of the source. It reaches only the
|
|
46
|
+
* collection: past that the station is already free, so there is nothing left for it to release.
|
|
37
47
|
*/
|
|
38
48
|
export declare function captureSnapshotFromShared(source: SharedLiveSource, opts?: {
|
|
39
49
|
timeoutMs?: number;
|
|
40
50
|
collectMs?: number;
|
|
41
51
|
skipKeyframes?: number;
|
|
52
|
+
signal?: AbortSignal;
|
|
42
53
|
logger?: Logger;
|
|
43
54
|
ffmpegLevel?: FfmpegLevel;
|
|
44
55
|
ffmpegPath?: string;
|
|
@@ -208,6 +208,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
208
208
|
* cameras from that second group streamed normally at level-1 — including one of the same firmware as an
|
|
209
209
|
* own-session camera that delivered no video at all for a reason of its own. An expired grace therefore
|
|
210
210
|
* separates nothing on this path, and a start failure on such a session is not evidence about it.
|
|
211
|
+
*
|
|
212
|
+
* Every `false` answer carries a `level2-unavailable` trace naming its reason, wherever the wait ended: a
|
|
213
|
+
* `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
|
|
214
|
+
* cipher and the cause are known, and re-stating it here would double every settled negotiation.
|
|
211
215
|
*/
|
|
212
216
|
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session"): Promise<boolean>;
|
|
213
217
|
/**
|
|
@@ -26,24 +26,35 @@ export interface SessionManagerOpts {
|
|
|
26
26
|
batteryIdleMs?: number;
|
|
27
27
|
/** Keepalive a single command holds after dispatch (ms). Default {@link COMMAND_KEEPALIVE_MS}. */
|
|
28
28
|
commandKeepAliveMs?: number;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Power tier per station serial — injected by the facade (no model import). Default: everything
|
|
31
|
+
* `wired`. Asked about the STATION a session connects to, never the key it is filed under, so every
|
|
32
|
+
* session to one station gets that station's idle window.
|
|
33
|
+
*/
|
|
30
34
|
poweredFor?: (parentSn: string) => PowerTier;
|
|
31
35
|
/**
|
|
32
|
-
* Called
|
|
33
|
-
* deferred reset falling due.
|
|
36
|
+
* Called with the KEY of a session the manager closed on its OWN initiative — an elapsed idle window,
|
|
37
|
+
* or a deferred reset falling due. The key, not the station: several sessions can share a station and
|
|
38
|
+
* only the one that closed is stale, so an owner told the station would tear down connections that are
|
|
39
|
+
* still serving.
|
|
34
40
|
*
|
|
35
41
|
* Those two are the only closes with no caller to follow up: everything riding the session is stale the
|
|
36
42
|
* moment it goes, and only the owner knows what that is. A close a caller asked for is that caller's to
|
|
37
43
|
* clean up after, which is why this does not fire for {@link SessionManager.close},
|
|
38
44
|
* {@link SessionManager.closeAll}, or a superseded open.
|
|
39
45
|
*/
|
|
40
|
-
onAutoClose?: (
|
|
46
|
+
onAutoClose?: (key: string) => void;
|
|
41
47
|
/** Diagnostics sink for the lifecycle transitions (open / idle-arm / detach). Omit for silence. */
|
|
42
48
|
logger?: Logger;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
|
-
* Manages P2P sessions
|
|
46
|
-
* `P2PSession` (it owns the socket
|
|
51
|
+
* Manages P2P sessions by **key**, each recording the station it connects to. The router builds and
|
|
52
|
+
* wires the actual `P2PSession` (it owns the socket and event fan-out); this decides open/close timing.
|
|
53
|
+
*
|
|
54
|
+
* A key is the station's serial for the one session that carries its control traffic and its events.
|
|
55
|
+
* Where a station has to serve more than one camera at once it also holds a session per camera, filed
|
|
56
|
+
* under a key of the router's choosing and recording the same station — so each has its own refcount and
|
|
57
|
+
* its own idle window, and releasing one never disturbs another.
|
|
47
58
|
*/
|
|
48
59
|
export declare class SessionManager {
|
|
49
60
|
private readonly opts;
|
|
@@ -53,23 +64,40 @@ export declare class SessionManager {
|
|
|
53
64
|
private readonly logger;
|
|
54
65
|
constructor(opts?: SessionManagerOpts);
|
|
55
66
|
/** The live session for a station, or `undefined` if not open. */
|
|
56
|
-
get(
|
|
57
|
-
/**
|
|
67
|
+
get(key: string): P2PSession | undefined;
|
|
68
|
+
/** Keys of the open sessions. */
|
|
58
69
|
keys(): string[];
|
|
59
|
-
/** A plain `Map<
|
|
70
|
+
/** A plain `Map<key, P2PSession>` snapshot of the open sessions (for `getSessions()` / tests). */
|
|
60
71
|
liveSessions(): Map<string, P2PSession>;
|
|
61
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Get or create the lifecycle entry under `key`, recording which station it connects to. An existing
|
|
74
|
+
* entry keeps the station it was opened with — the key owns one connection for its lifetime, and a
|
|
75
|
+
* later caller passing a different station would otherwise re-point a live entry's power tier.
|
|
76
|
+
*/
|
|
62
77
|
private entry;
|
|
63
|
-
/** Register an already-built session for test seeding or an externally assembled connection. */
|
|
64
|
-
register(parentSn: string, session: P2PSession): void;
|
|
65
78
|
/**
|
|
66
|
-
*
|
|
79
|
+
* Register an already-built session for test seeding or an externally assembled connection.
|
|
80
|
+
*
|
|
81
|
+
* `station` defaults to the key, which is safe HERE and nowhere else in this class: the paths that open a
|
|
82
|
+
* session under a key that is not a station serial all go through {@link acquire}, which requires it. A
|
|
83
|
+
* caller seeding one under such a key must pass it.
|
|
84
|
+
*/
|
|
85
|
+
register(key: string, session: P2PSession, station?: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* Ensure a session to `key` is open, building it via `factory` if cold. Concurrent calls for the
|
|
67
88
|
* same cold station share ONE connect (the `connecting` promise); `factory` builds + wires + awaits
|
|
68
89
|
* `connect()` and resolves the connected session.
|
|
69
90
|
*/
|
|
70
|
-
acquire(
|
|
71
|
-
/**
|
|
72
|
-
|
|
91
|
+
acquire(key: string, factory: (register: (session: P2PSession) => void) => Promise<P2PSession>, station: string): Promise<P2PSession>;
|
|
92
|
+
/**
|
|
93
|
+
* Add a reason to stay connected; cancels a pending idle-close.
|
|
94
|
+
*
|
|
95
|
+
* Refused when nothing is open under `key`, for the reason {@link release} gives in the other direction: a
|
|
96
|
+
* retain names a session that was acquired, and one that names nothing would file an entry with no
|
|
97
|
+
* connection behind it. {@link hold} is the path that legitimately creates one, and it opens the entry
|
|
98
|
+
* itself before retaining it.
|
|
99
|
+
*/
|
|
100
|
+
retain(key: string): void;
|
|
73
101
|
/**
|
|
74
102
|
* Release a reason; arm the idle-close when the last one goes.
|
|
75
103
|
*
|
|
@@ -78,22 +106,28 @@ export declare class SessionManager {
|
|
|
78
106
|
* from scratch or complete a deferred reset a real viewer has not yet earned. Clamping to zero did
|
|
79
107
|
* both silently.
|
|
80
108
|
*/
|
|
81
|
-
release(
|
|
109
|
+
release(key: string): void;
|
|
82
110
|
/**
|
|
83
111
|
* Hold a session warm for `commandKeepAliveMs` after a control command, then release. A burst of
|
|
84
112
|
* commands each re-holds before the previous release fires, so the session never idles mid-burst.
|
|
85
113
|
*/
|
|
86
|
-
bumpCommand(
|
|
114
|
+
bumpCommand(key: string, station: string): void;
|
|
87
115
|
/**
|
|
88
|
-
* Retain a
|
|
116
|
+
* Retain a session and release it again after `ms` — the primitive behind command-keepalive and event
|
|
89
117
|
* pre-warm, and the only way to hold one open without an attachment to release it.
|
|
90
118
|
*
|
|
119
|
+
* `station` is required rather than defaulted from the key, because this is the one path that can
|
|
120
|
+
* CREATE an entry: a pre-warm takes its hold before the open. An entry filed under a media key with
|
|
121
|
+
* that key as its own station would be asked for the power tier of a serial that does not exist, be
|
|
122
|
+
* answered `wired`, and never idle-detach — which on a battery station is the drain this class exists
|
|
123
|
+
* to prevent, and is invisible until the battery is flat.
|
|
124
|
+
*
|
|
91
125
|
* The timer is owned by the entry, so {@link discard} cancels it. That ownership is the point: keyed
|
|
92
126
|
* only by serial, an expiring hold would otherwise outlive the entry it was taken on and release a
|
|
93
127
|
* retain counted by the SUCCESSOR entry — dropping a live viewer's count and arming an idle-detach
|
|
94
128
|
* underneath it.
|
|
95
129
|
*/
|
|
96
|
-
hold(
|
|
130
|
+
hold(key: string, ms: number, station: string): void;
|
|
97
131
|
/**
|
|
98
132
|
* Arm the idle-close timer for a station whose retain count just reached zero. A wired station with
|
|
99
133
|
* an infinite window is left persistent (no timer). Any subsequent {@link retain} cancels it.
|
|
@@ -123,9 +157,9 @@ export declare class SessionManager {
|
|
|
123
157
|
*/
|
|
124
158
|
private autoClose;
|
|
125
159
|
/** Drop a station's entry + timer (called from the session's `close` handler). Idempotent. */
|
|
126
|
-
remove(
|
|
160
|
+
remove(key: string): void;
|
|
127
161
|
/** Close one station now and discard its lifecycle entry. */
|
|
128
|
-
close(
|
|
162
|
+
close(key: string): Promise<void>;
|
|
129
163
|
/**
|
|
130
164
|
* Reset once every viewer detaches, ignoring only expiring holds.
|
|
131
165
|
*
|
|
@@ -136,7 +170,7 @@ export declare class SessionManager {
|
|
|
136
170
|
* Both branches close through {@link autoClose}: the caller asked for a recycle, not for the station's
|
|
137
171
|
* live sources to be dropped, so it does not clean up after one — exactly like the idle path.
|
|
138
172
|
*/
|
|
139
|
-
resetWhenUnused(
|
|
173
|
+
resetWhenUnused(key: string): Promise<void>;
|
|
140
174
|
/** Settle a discarded entry's reset callers with the same outcome as its session close. */
|
|
141
175
|
private settleReset;
|
|
142
176
|
/** Close one discarded entry and settle only its own reset callers before preserving any failure. */
|
|
@@ -10,7 +10,7 @@ export interface SharedLiveSourceOptions {
|
|
|
10
10
|
* `.start()` itself.
|
|
11
11
|
*
|
|
12
12
|
* `ctx.reassertWanted` answers whether this pull still has anyone attached. A stream that re-asserts a
|
|
13
|
-
* channel to hold it open should consult it, so a pull nothing is watching stops competing for a
|
|
13
|
+
* channel to hold it open should consult it, so a pull nothing is watching stops competing for a session
|
|
14
14
|
* that serves one camera at a time.
|
|
15
15
|
*/
|
|
16
16
|
makeStream: (ctx: {
|
|
@@ -73,6 +73,15 @@ export interface SharedLiveSourceOptions {
|
|
|
73
73
|
* an owner does in response to this callback.
|
|
74
74
|
*/
|
|
75
75
|
onStartFailed?: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* The pull has ended and will not resume: the linger elapsed, the battery budget ran out, or the source
|
|
78
|
+
* was torn down. A later attach builds a fresh stream rather than reviving this one.
|
|
79
|
+
*
|
|
80
|
+
* Distinct from {@link onIdle} by what is still running. `onIdle` fires at the last detach, while the
|
|
81
|
+
* linger is still holding the pull open so a quick re-attach costs nothing; between the two the pull is
|
|
82
|
+
* alive. This fires when it is not.
|
|
83
|
+
*/
|
|
84
|
+
onStopped?: () => void;
|
|
76
85
|
/**
|
|
77
86
|
* A media start was abandoned unacknowledged before anything was delivered, so this session is not being
|
|
78
87
|
* heard. The owner is asked for a replacement and calls {@link SharedLiveSource.rewarm} once it has one.
|