@mega-yfue/eufy-sdk 0.2.0-beta.10 → 0.2.0-beta.12
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 +11 -6
- package/dist/core/contracts.d.ts +9 -34
- package/dist/index.js +363 -130
- package/dist/index.js.map +2 -2
- package/dist/model/capabilities/battery.d.ts +32 -4
- package/dist/model/capabilities/index.d.ts +9 -0
- package/dist/model/device.d.ts +15 -0
- package/dist/transport/p2p/command-router.d.ts +120 -15
- package/dist/transport/p2p/live-stream.d.ts +5 -4
- package/dist/transport/p2p/media.d.ts +2 -2
- package/dist/transport/p2p/session-manager.d.ts +57 -23
- package/dist/transport/p2p/shared-live-source.d.ts +10 -1
- package/package.json +1 -1
|
@@ -90,8 +90,33 @@ export type BatteryActions = Surface<typeof BATTERY_MEMBERS>;
|
|
|
90
90
|
* The richer raw `APP_CMD_SET_POWER_SOURCE` blob is a separate param, surfaced as `powerSourceInfo`.
|
|
91
91
|
*/
|
|
92
92
|
declare function decodePowerSource(raw: string | number | boolean): number | string;
|
|
93
|
-
/**
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The params whose subject IS the physical cell — so every member reading one must carry
|
|
95
|
+
* {@link notMainsCamera}.
|
|
96
|
+
*
|
|
97
|
+
* The ONE place that fact is declared. {@link cellGated} applies {@link notMainsCamera} from this list
|
|
98
|
+
* when the table is built, so a member never states the gate itself: adding a cell read is adding its
|
|
99
|
+
* param here, and there is no second place for it to be missing from. A per-member `available` was the
|
|
100
|
+
* alternative and is what the first two passes of this guard got wrong, in both directions — first by
|
|
101
|
+
* covering two of the seven, then by reading `unexposed` as covering a third.
|
|
102
|
+
*
|
|
103
|
+
* What is NOT here matters as much.
|
|
104
|
+
*
|
|
105
|
+
* - `workingMode` and the three `record*` settings describe how hard the camera works, not what powers
|
|
106
|
+
* it, and a mains camera genuinely has them. They are the reason the capability stays attached.
|
|
107
|
+
* - `cameraInfo` (1103) is a number whose meaning is unevidenced. Gating it would assert it is a
|
|
108
|
+
* battery fact, which is the kind of claim this guard exists to stop making.
|
|
109
|
+
* - `powerSource` (1293) is the open one. Both values it names — `Battery` and `External Solar Panel` —
|
|
110
|
+
* describe how a CELL is fed, so by this list's own rule it arguably belongs here. It is out because
|
|
111
|
+
* every param above is one a live mains camera was observed to publish and 1293 was not among them,
|
|
112
|
+
* and because it is `requires`-gated on its own param, so it appears only where the device reports
|
|
113
|
+
* it. Gating it would also withhold a described WRITE rather than a read, which is a different class
|
|
114
|
+
* of change. Unresolved rather than decided: see the note in the pull request.
|
|
115
|
+
*
|
|
116
|
+
* The two solar params ARE here: a panel exists to charge a cell, so a device without one has no solar
|
|
117
|
+
* harvest to report either.
|
|
118
|
+
*/
|
|
119
|
+
export declare const CELL_PARAMS: readonly number[];
|
|
95
120
|
/**
|
|
96
121
|
* Every `battery` feature, declared once — the property schema, the evidence-gated getters, the derived
|
|
97
122
|
* setters, the intent routes and the descriptions all come out of this table. Order is schema order.
|
|
@@ -117,7 +142,6 @@ export declare const BATTERY_MEMBERS: {
|
|
|
117
142
|
readonly unit: "%";
|
|
118
143
|
readonly kind: "percent";
|
|
119
144
|
readonly provenance: "verified";
|
|
120
|
-
readonly available: typeof notMainsCamera;
|
|
121
145
|
readonly description: "Battery level 0-100 (verified: param 1101).";
|
|
122
146
|
};
|
|
123
147
|
/**
|
|
@@ -130,7 +154,6 @@ export declare const BATTERY_MEMBERS: {
|
|
|
130
154
|
readonly type: "bool";
|
|
131
155
|
readonly kind: "boolean";
|
|
132
156
|
readonly provenance: "apk";
|
|
133
|
-
readonly available: typeof notMainsCamera;
|
|
134
157
|
readonly coerce: (v: string | number | boolean) => boolean;
|
|
135
158
|
readonly description: string;
|
|
136
159
|
};
|
|
@@ -300,6 +323,11 @@ export declare const BATTERY_MEMBERS: {
|
|
|
300
323
|
* Reported, so it stays in the schema and answers through `getProperty` — but given no typed getter:
|
|
301
324
|
* the payload's fields have never been decoded, and a getter would hand back an opaque blob typed as
|
|
302
325
|
* though it meant something.
|
|
326
|
+
*
|
|
327
|
+
* `unexposed` is NOT a substitute for the cell gate. It suppresses the fluent GETTER; `propertiesOf`
|
|
328
|
+
* filters `writeOnly` and `available` and deliberately not `unexposed`, because a schema entry
|
|
329
|
+
* reachable through `getProperty` is the whole point of the mark. So a cell param needs
|
|
330
|
+
* {@link CELL_PARAMS} either way, or a mains camera publishes "battery power history" and answers it.
|
|
303
331
|
*/
|
|
304
332
|
readonly batteryPowerStats: {
|
|
305
333
|
readonly param: 3100;
|
|
@@ -54,6 +54,15 @@ export declare function getCapabilityModule(cap: Capability): CapabilityModule |
|
|
|
54
54
|
* @internal
|
|
55
55
|
*/
|
|
56
56
|
export declare const CAPABILITY_MODULES: Record<Capability, CapabilityModule>;
|
|
57
|
+
/**
|
|
58
|
+
* Every param these capabilities declare before any gate — the set a device's schema is a subset of.
|
|
59
|
+
*
|
|
60
|
+
* A param in here that a device's schema does NOT carry is one a gate withheld: the capability resolved,
|
|
61
|
+
* and its member decided the read does not describe this device — a cell reading on a mains model, a mode
|
|
62
|
+
* a family does not carry. Read-aliases count, since a member reads them under its own name.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export declare function claimedParams(caps: Capability[]): Set<number>;
|
|
57
66
|
/**
|
|
58
67
|
* Merge the property schemas of several capabilities into one flat, de-duplicated list.
|
|
59
68
|
*
|
package/dist/model/device.d.ts
CHANGED
|
@@ -62,6 +62,11 @@ export declare class Device {
|
|
|
62
62
|
* different wire ids across device families still resolves to one named value.
|
|
63
63
|
*/
|
|
64
64
|
private specByParam;
|
|
65
|
+
/**
|
|
66
|
+
* Params a resolved capability declares that this device's schema does not carry — withheld by a
|
|
67
|
+
* member's gate, so not named from the param dictionary either. See {@link Device.applyParams}.
|
|
68
|
+
*/
|
|
69
|
+
private withheld;
|
|
65
70
|
/** Which param namespace this device's ids live in (clean DPs vs security P2P). */
|
|
66
71
|
private namespace;
|
|
67
72
|
/** The record's `device_name` as stated, before the {@link modelName} fallback is applied. */
|
|
@@ -201,6 +206,16 @@ export declare class Device {
|
|
|
201
206
|
* Apply a raw param map (cloud record or P2P notification). Known params update their named
|
|
202
207
|
* property; unrecognised params are retained as `unknown_<paramType>` so nothing is lost.
|
|
203
208
|
*
|
|
209
|
+
* Naming precedence: this device's own `PropertySpec` (curated), then the param dictionary for its
|
|
210
|
+
* namespace, then the `unknown_<paramType>` passthrough. The dictionary def is consulted even where a
|
|
211
|
+
* spec exists, because `encoding` lives there.
|
|
212
|
+
*
|
|
213
|
+
* A param a resolved capability's gate WITHHELD takes the passthrough instead of its dictionary name.
|
|
214
|
+
* The gate decided the read does not describe this device, the dictionary names it what the member
|
|
215
|
+
* would have, and republishing it there hands a caller a reading indistinguishable from one the device
|
|
216
|
+
* really answered. A capability that never resolved withholds nothing: a param arriving before its
|
|
217
|
+
* capability is still the device's own, and keeps its dictionary name.
|
|
218
|
+
*
|
|
204
219
|
* @param params param_type → raw value.
|
|
205
220
|
* @param ts observation time (epoch ms); defaults to `Date.now()`.
|
|
206
221
|
* @returns the list of property names whose value changed.
|
|
@@ -88,7 +88,7 @@ export interface P2PRouterDeps {
|
|
|
88
88
|
}
|
|
89
89
|
export declare class P2PCommandRouter {
|
|
90
90
|
private readonly deps;
|
|
91
|
-
/**
|
|
91
|
+
/** P2P session lifecycle: on-demand open + battery-aware idle-detach + refcount, per session key. */
|
|
92
92
|
private readonly manager;
|
|
93
93
|
/** Error objects already forwarded while a station startup awaits the same session signal. */
|
|
94
94
|
private readonly reportedErrors;
|
|
@@ -96,6 +96,21 @@ export declare class P2PCommandRouter {
|
|
|
96
96
|
private readonly liveSources;
|
|
97
97
|
/** The options each live source was built from, so a later caller's conflicting ones can be reported. */
|
|
98
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;
|
|
99
114
|
/**
|
|
100
115
|
* The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
|
|
101
116
|
* time and the session carries one audio sequence, so this path is exclusive where a live pull is
|
|
@@ -122,7 +137,10 @@ export declare class P2PCommandRouter {
|
|
|
122
137
|
* `no P2P session`.
|
|
123
138
|
*/
|
|
124
139
|
static claimsDevice(dev: EufyDevice): boolean;
|
|
125
|
-
/**
|
|
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
|
+
*/
|
|
126
144
|
getSessions(): Map<string, P2PSession>;
|
|
127
145
|
/**
|
|
128
146
|
* Speculatively open + briefly hold a station's session (e.g. after a doorbell ring) so a
|
|
@@ -132,7 +150,7 @@ export declare class P2PCommandRouter {
|
|
|
132
150
|
*
|
|
133
151
|
* One hold, taken before the open so a slow connect can't idle-close mid-flight. It expires on its
|
|
134
152
|
* own, which arms the station's idle window rather than closing the session, per {@link PREWARM_MS}.
|
|
135
|
-
* 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
|
|
136
154
|
* and the lookups are away, not once the peer has answered, so both would expire together.
|
|
137
155
|
*
|
|
138
156
|
* Best-effort — a failed open surfaces via `onError`. A {@link SessionSupersededError} does not: the
|
|
@@ -165,7 +183,41 @@ export declare class P2PCommandRouter {
|
|
|
165
183
|
* when present, else the freshest private IP in the record ({@link freshestLanIp}) — so P2P works
|
|
166
184
|
* on-LAN even when broadcast is blocked (AP isolation) or the record's `ip_addr` went stale.
|
|
167
185
|
*/
|
|
168
|
-
|
|
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;
|
|
169
221
|
/**
|
|
170
222
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
171
223
|
*
|
|
@@ -177,6 +229,12 @@ export declare class P2PCommandRouter {
|
|
|
177
229
|
*
|
|
178
230
|
* The `close` handler drops the session from the {@link SessionManager} and disposes any shared live
|
|
179
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.
|
|
180
238
|
*/
|
|
181
239
|
private makeSession;
|
|
182
240
|
/**
|
|
@@ -285,22 +343,40 @@ export declare class P2PCommandRouter {
|
|
|
285
343
|
* {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
|
|
286
344
|
* reuse branch, so a reuse frees the station as a cold start does.
|
|
287
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
|
+
*
|
|
288
353
|
* The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
|
|
289
354
|
* place.
|
|
290
355
|
*/
|
|
291
|
-
sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts): Promise<SharedLiveSource>;
|
|
356
|
+
sharedLiveSourceFor(sn: string, opts?: SharedLiveOpts, mayOpenOwnSession?: boolean): Promise<SharedLiveSource>;
|
|
292
357
|
/**
|
|
293
358
|
* Tear down any pull on this station that is lingering for ANOTHER camera, before starting this one.
|
|
294
359
|
*
|
|
295
360
|
* A lingering pull has no consumers but is still held open, and on an attached camera holding it open means
|
|
296
|
-
* re-sending the full media start every keepalive tick. Two channels doing that at once
|
|
297
|
-
* serves one camera at a time leaves the new stream receiving nothing but the old camera's frames
|
|
298
|
-
* 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.
|
|
299
364
|
*
|
|
300
365
|
* Several cameras genuinely being WATCHED together are never disturbed — the linger exists to make
|
|
301
366
|
* re-opening the SAME camera cheap, and it keeps doing that. What it may not do is keep a camera nobody is
|
|
302
367
|
* looking at competing with one somebody just asked for.
|
|
303
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
|
+
*
|
|
304
380
|
* A snapshot tile is nobody looking. Opening a live view in the Home app takes that cell fullscreen, so the
|
|
305
381
|
* pulls refreshing the other cells are off screen, yet each goes on re-issuing its own media start every
|
|
306
382
|
* retry tick — measured as four pulls warming together off one HomeBase, a live request landing 1.4 s later,
|
|
@@ -311,14 +387,27 @@ export declare class P2PCommandRouter {
|
|
|
311
387
|
*/
|
|
312
388
|
private releaseLingeringSiblings;
|
|
313
389
|
/**
|
|
314
|
-
*
|
|
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.
|
|
315
404
|
*
|
|
316
405
|
* A stopped source is skipped even when consumers are still attached to it. A failed start fails its
|
|
317
406
|
* consumers without detaching them, so a caller still holding a dead handle leaves the count non-zero,
|
|
318
|
-
* and counting that as a viewer would
|
|
319
|
-
* 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.
|
|
320
409
|
*/
|
|
321
|
-
private
|
|
410
|
+
private stationSessionInUse;
|
|
322
411
|
/**
|
|
323
412
|
* Whether the stream on `key` should re-assert its channel to hold the station.
|
|
324
413
|
*
|
|
@@ -328,8 +417,8 @@ export declare class P2PCommandRouter {
|
|
|
328
417
|
* - Nothing attached: no. There is nobody to take the station for.
|
|
329
418
|
* - A live viewer attached: yes. That is the picture someone is looking at.
|
|
330
419
|
* - Held only for stills, while a sibling on this station has a live viewer: no. A still refreshes a
|
|
331
|
-
* tile that is off screen while the live view is on it, and
|
|
332
|
-
* 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
|
|
333
422
|
* it took, and its own capture then took fifteen seconds because it was contending.
|
|
334
423
|
*
|
|
335
424
|
* A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
|
|
@@ -344,8 +433,24 @@ export declare class P2PCommandRouter {
|
|
|
344
433
|
* not.
|
|
345
434
|
*/
|
|
346
435
|
private attachUnlessAborted;
|
|
347
|
-
/**
|
|
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
|
+
*/
|
|
348
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;
|
|
349
454
|
/**
|
|
350
455
|
* Drop everything that was riding a station's session, and report the station closed.
|
|
351
456
|
*
|
|
@@ -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
|
*
|
|
@@ -36,8 +36,8 @@ export declare function openLiveStream(session: P2PSession, opts?: LiveStreamOpt
|
|
|
36
36
|
* image, so the image is its source of truth.
|
|
37
37
|
*
|
|
38
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.
|
|
40
|
-
*
|
|
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
41
|
* that station to every live request for the length of an FFmpeg run — measured on a real base as a live
|
|
42
42
|
* request refused 370ms after the still it was waiting on had already collected everything it needed.
|
|
43
43
|
*
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.12",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|