@mega-yfue/eufy-sdk 0.2.0-beta.10 → 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 +11 -6
- package/dist/core/contracts.d.ts +9 -34
- package/dist/index.js +307 -124
- package/dist/index.js.map +2 -2
- 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
package/dist/index.js
CHANGED
|
@@ -2615,16 +2615,6 @@ var StationUnreachableError = class extends Error {
|
|
|
2615
2615
|
this.name = "StationUnreachableError";
|
|
2616
2616
|
}
|
|
2617
2617
|
};
|
|
2618
|
-
var StationBusyError = class extends Error {
|
|
2619
|
-
servingChannel;
|
|
2620
|
-
/** Always true: the station is busy now, and stops being busy when the other stream is released. */
|
|
2621
|
-
retryable = true;
|
|
2622
|
-
constructor(servingChannel, options) {
|
|
2623
|
-
super(`the station is already serving channel ${servingChannel} to a viewer, and serves one camera at a time \u2014 stop that stream before opening another`, options);
|
|
2624
|
-
this.servingChannel = servingChannel;
|
|
2625
|
-
this.name = "StationBusyError";
|
|
2626
|
-
}
|
|
2627
|
-
};
|
|
2628
2618
|
var LiveStreamStartError = class extends Error {
|
|
2629
2619
|
reason;
|
|
2630
2620
|
stage;
|
|
@@ -16785,8 +16775,9 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
16785
16775
|
* Stop re-issuing the media start once this camera's own media has arrived, on an attached camera.
|
|
16786
16776
|
*
|
|
16787
16777
|
* The nudge differs by topology and only one branch is a ping: an own-session camera sends a small
|
|
16788
|
-
* keepalive, while an attached camera has no such state and re-sends the FULL media start.
|
|
16789
|
-
* serves one camera at a time that restart re-asserts this channel against whatever else is warm
|
|
16778
|
+
* keepalive, while an attached camera has no such state and re-sends the FULL media start. Over one session,
|
|
16779
|
+
* which serves one camera at a time, that restart re-asserts this channel against whatever else is warm on
|
|
16780
|
+
* it, so two
|
|
16790
16781
|
* attached streams restart every interval and contend for the station continuously — measured on a real base
|
|
16791
16782
|
* as a full start every 3 s from each.
|
|
16792
16783
|
*
|
|
@@ -16953,7 +16944,7 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
16953
16944
|
*
|
|
16954
16945
|
* The match is UNCONDITIONAL, however long a station serves another camera instead of this one.
|
|
16955
16946
|
*
|
|
16956
|
-
*
|
|
16947
|
+
* One session serving one camera at a time hands a newly opened camera nothing but its sibling's frames until
|
|
16957
16948
|
* it switches, so "no media of my own yet, plenty for someone else" is what an ordinary handover looks like
|
|
16958
16949
|
* and does not distinguish a station that tags differently from one that is simply busy.
|
|
16959
16950
|
*
|
|
@@ -18003,6 +17994,8 @@ var SharedLiveSource = class {
|
|
|
18003
17994
|
this.configuredFrom = void 0;
|
|
18004
17995
|
this.ring = [];
|
|
18005
17996
|
this._state = state;
|
|
17997
|
+
if (state === "stopped")
|
|
17998
|
+
this.opts.onStopped?.();
|
|
18006
17999
|
if (startFailed && report)
|
|
18007
18000
|
this.opts.onStartFailed?.();
|
|
18008
18001
|
}
|
|
@@ -18085,14 +18078,14 @@ var SessionManager = class {
|
|
|
18085
18078
|
this.logger = opts.logger ?? noopLogger;
|
|
18086
18079
|
}
|
|
18087
18080
|
/** The live session for a station, or `undefined` if not open. */
|
|
18088
|
-
get(
|
|
18089
|
-
return this.entries.get(
|
|
18081
|
+
get(key) {
|
|
18082
|
+
return this.entries.get(key)?.session;
|
|
18090
18083
|
}
|
|
18091
|
-
/**
|
|
18084
|
+
/** Keys of the open sessions. */
|
|
18092
18085
|
keys() {
|
|
18093
18086
|
return [...this.entries].filter(([, e]) => e.session).map(([sn]) => sn);
|
|
18094
18087
|
}
|
|
18095
|
-
/** A plain `Map<
|
|
18088
|
+
/** A plain `Map<key, P2PSession>` snapshot of the open sessions (for `getSessions()` / tests). */
|
|
18096
18089
|
liveSessions() {
|
|
18097
18090
|
const m = /* @__PURE__ */ new Map();
|
|
18098
18091
|
for (const [sn, e] of this.entries)
|
|
@@ -18100,56 +18093,77 @@ var SessionManager = class {
|
|
|
18100
18093
|
m.set(sn, e.session);
|
|
18101
18094
|
return m;
|
|
18102
18095
|
}
|
|
18103
|
-
/**
|
|
18104
|
-
|
|
18105
|
-
|
|
18096
|
+
/**
|
|
18097
|
+
* Get or create the lifecycle entry under `key`, recording which station it connects to. An existing
|
|
18098
|
+
* entry keeps the station it was opened with — the key owns one connection for its lifetime, and a
|
|
18099
|
+
* later caller passing a different station would otherwise re-point a live entry's power tier.
|
|
18100
|
+
*/
|
|
18101
|
+
entry(key, station) {
|
|
18102
|
+
let e = this.entries.get(key);
|
|
18106
18103
|
if (!e) {
|
|
18107
|
-
e = { retained: 0, holdTimers: /* @__PURE__ */ new Set(), idle: new Timer() };
|
|
18108
|
-
this.entries.set(
|
|
18104
|
+
e = { station, retained: 0, holdTimers: /* @__PURE__ */ new Set(), idle: new Timer() };
|
|
18105
|
+
this.entries.set(key, e);
|
|
18109
18106
|
}
|
|
18110
18107
|
return e;
|
|
18111
18108
|
}
|
|
18112
|
-
/**
|
|
18113
|
-
|
|
18114
|
-
|
|
18109
|
+
/**
|
|
18110
|
+
* Register an already-built session for test seeding or an externally assembled connection.
|
|
18111
|
+
*
|
|
18112
|
+
* `station` defaults to the key, which is safe HERE and nowhere else in this class: the paths that open a
|
|
18113
|
+
* session under a key that is not a station serial all go through {@link acquire}, which requires it. A
|
|
18114
|
+
* caller seeding one under such a key must pass it.
|
|
18115
|
+
*/
|
|
18116
|
+
register(key, session, station = key) {
|
|
18117
|
+
this.entry(key, station).session = session;
|
|
18115
18118
|
}
|
|
18116
18119
|
/**
|
|
18117
|
-
* Ensure a session to `
|
|
18120
|
+
* Ensure a session to `key` is open, building it via `factory` if cold. Concurrent calls for the
|
|
18118
18121
|
* same cold station share ONE connect (the `connecting` promise); `factory` builds + wires + awaits
|
|
18119
18122
|
* `connect()` and resolves the connected session.
|
|
18120
18123
|
*/
|
|
18121
|
-
async acquire(
|
|
18122
|
-
const e = this.entry(
|
|
18124
|
+
async acquire(key, factory, station) {
|
|
18125
|
+
const e = this.entry(key, station);
|
|
18123
18126
|
if (e.connecting) {
|
|
18124
|
-
this.logger.debug(`[session ${
|
|
18127
|
+
this.logger.debug(`[session ${key}] connecting \u2014 joining in-flight open`);
|
|
18125
18128
|
return e.connecting;
|
|
18126
18129
|
}
|
|
18127
18130
|
if (e.session)
|
|
18128
18131
|
return e.session;
|
|
18129
|
-
this.logger.debug(`[session ${
|
|
18132
|
+
this.logger.debug(`[session ${key}] connecting now (on demand)`);
|
|
18130
18133
|
const generation = this.generation;
|
|
18131
18134
|
const p = factory((session) => e.session = session);
|
|
18132
18135
|
e.connecting = p;
|
|
18133
18136
|
try {
|
|
18134
18137
|
const session = await p;
|
|
18135
|
-
if (generation !== this.generation || this.entries.get(
|
|
18138
|
+
if (generation !== this.generation || this.entries.get(key) !== e) {
|
|
18136
18139
|
await session.close();
|
|
18137
|
-
throw new SessionSupersededError(`P2P session start superseded for
|
|
18140
|
+
throw new SessionSupersededError(`P2P session start superseded for ${key}`);
|
|
18138
18141
|
}
|
|
18139
18142
|
e.session ??= session;
|
|
18140
|
-
this.logger.debug(`[session ${
|
|
18143
|
+
this.logger.debug(`[session ${key}] connected`);
|
|
18141
18144
|
return e.session;
|
|
18142
18145
|
} finally {
|
|
18143
18146
|
if (e.connecting === p)
|
|
18144
18147
|
e.connecting = void 0;
|
|
18145
18148
|
}
|
|
18146
18149
|
}
|
|
18147
|
-
/**
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
+
/**
|
|
18151
|
+
* Add a reason to stay connected; cancels a pending idle-close.
|
|
18152
|
+
*
|
|
18153
|
+
* Refused when nothing is open under `key`, for the reason {@link release} gives in the other direction: a
|
|
18154
|
+
* retain names a session that was acquired, and one that names nothing would file an entry with no
|
|
18155
|
+
* connection behind it. {@link hold} is the path that legitimately creates one, and it opens the entry
|
|
18156
|
+
* itself before retaining it.
|
|
18157
|
+
*/
|
|
18158
|
+
retain(key) {
|
|
18159
|
+
const e = this.entries.get(key);
|
|
18160
|
+
if (!e) {
|
|
18161
|
+
this.logger.warn(`[session ${key}] retain on a session that is not open \u2014 ignored`);
|
|
18162
|
+
return;
|
|
18163
|
+
}
|
|
18150
18164
|
e.retained++;
|
|
18151
18165
|
if (e.idle.pending)
|
|
18152
|
-
this.logger.debug(`[session ${
|
|
18166
|
+
this.logger.debug(`[session ${key}] in use again \u2014 idle-detach cancelled`);
|
|
18153
18167
|
e.idle.cancel();
|
|
18154
18168
|
}
|
|
18155
18169
|
/**
|
|
@@ -18160,44 +18174,50 @@ var SessionManager = class {
|
|
|
18160
18174
|
* from scratch or complete a deferred reset a real viewer has not yet earned. Clamping to zero did
|
|
18161
18175
|
* both silently.
|
|
18162
18176
|
*/
|
|
18163
|
-
release(
|
|
18164
|
-
const e = this.entries.get(
|
|
18177
|
+
release(key) {
|
|
18178
|
+
const e = this.entries.get(key);
|
|
18165
18179
|
if (!e)
|
|
18166
18180
|
return;
|
|
18167
18181
|
if (e.retained === 0) {
|
|
18168
|
-
this.logger.warn(`[session ${
|
|
18182
|
+
this.logger.warn(`[session ${key}] release with nothing retained \u2014 ignored`);
|
|
18169
18183
|
return;
|
|
18170
18184
|
}
|
|
18171
18185
|
e.retained -= 1;
|
|
18172
18186
|
if (e.reset && e.retained <= e.holdTimers.size) {
|
|
18173
|
-
void this.autoClose(
|
|
18187
|
+
void this.autoClose(key).catch((error) => this.logger.error(`[session ${key}] deferred reset failed`, error));
|
|
18174
18188
|
return;
|
|
18175
18189
|
}
|
|
18176
18190
|
if (e.retained === 0)
|
|
18177
|
-
this.armIdle(
|
|
18191
|
+
this.armIdle(key, e);
|
|
18178
18192
|
}
|
|
18179
18193
|
/**
|
|
18180
18194
|
* Hold a session warm for `commandKeepAliveMs` after a control command, then release. A burst of
|
|
18181
18195
|
* commands each re-holds before the previous release fires, so the session never idles mid-burst.
|
|
18182
18196
|
*/
|
|
18183
|
-
bumpCommand(
|
|
18184
|
-
this.hold(
|
|
18197
|
+
bumpCommand(key, station) {
|
|
18198
|
+
this.hold(key, this.opts.commandKeepAliveMs ?? COMMAND_KEEPALIVE_MS, station);
|
|
18185
18199
|
}
|
|
18186
18200
|
/**
|
|
18187
|
-
* Retain a
|
|
18201
|
+
* Retain a session and release it again after `ms` — the primitive behind command-keepalive and event
|
|
18188
18202
|
* pre-warm, and the only way to hold one open without an attachment to release it.
|
|
18189
18203
|
*
|
|
18204
|
+
* `station` is required rather than defaulted from the key, because this is the one path that can
|
|
18205
|
+
* CREATE an entry: a pre-warm takes its hold before the open. An entry filed under a media key with
|
|
18206
|
+
* that key as its own station would be asked for the power tier of a serial that does not exist, be
|
|
18207
|
+
* answered `wired`, and never idle-detach — which on a battery station is the drain this class exists
|
|
18208
|
+
* to prevent, and is invisible until the battery is flat.
|
|
18209
|
+
*
|
|
18190
18210
|
* The timer is owned by the entry, so {@link discard} cancels it. That ownership is the point: keyed
|
|
18191
18211
|
* only by serial, an expiring hold would otherwise outlive the entry it was taken on and release a
|
|
18192
18212
|
* retain counted by the SUCCESSOR entry — dropping a live viewer's count and arming an idle-detach
|
|
18193
18213
|
* underneath it.
|
|
18194
18214
|
*/
|
|
18195
|
-
hold(
|
|
18196
|
-
const entry = this.entry(
|
|
18197
|
-
this.retain(
|
|
18215
|
+
hold(key, ms, station) {
|
|
18216
|
+
const entry = this.entry(key, station);
|
|
18217
|
+
this.retain(key);
|
|
18198
18218
|
const timer = setTimeout(() => {
|
|
18199
18219
|
entry.holdTimers.delete(timer);
|
|
18200
|
-
this.release(
|
|
18220
|
+
this.release(key);
|
|
18201
18221
|
}, ms);
|
|
18202
18222
|
timer.unref?.();
|
|
18203
18223
|
entry.holdTimers.add(timer);
|
|
@@ -18206,29 +18226,29 @@ var SessionManager = class {
|
|
|
18206
18226
|
* Arm the idle-close timer for a station whose retain count just reached zero. A wired station with
|
|
18207
18227
|
* an infinite window is left persistent (no timer). Any subsequent {@link retain} cancels it.
|
|
18208
18228
|
*/
|
|
18209
|
-
armIdle(
|
|
18229
|
+
armIdle(key, e) {
|
|
18210
18230
|
e.idle.cancel();
|
|
18211
|
-
if ((this.opts.poweredFor?.(
|
|
18212
|
-
this.logger.debug(`[session ${
|
|
18231
|
+
if ((this.opts.poweredFor?.(e.station) ?? "wired") !== "battery") {
|
|
18232
|
+
this.logger.debug(`[session ${key}] idle (nothing retained) \u2014 staying persistent (wired)`);
|
|
18213
18233
|
return;
|
|
18214
18234
|
}
|
|
18215
18235
|
const idleMs = this.opts.batteryIdleMs ?? BATTERY_IDLE_MS;
|
|
18216
|
-
this.logger.debug(`[session ${
|
|
18217
|
-
e.idle.arm(idleMs, () => this.onIdle(
|
|
18236
|
+
this.logger.debug(`[session ${key}] idle (nothing retained) \u2014 detaching in ${idleMs}ms unless reused`);
|
|
18237
|
+
e.idle.arm(idleMs, () => this.onIdle(key));
|
|
18218
18238
|
}
|
|
18219
18239
|
/**
|
|
18220
18240
|
* Close a station's session once its idle window elapses with nothing retained, letting the device sleep.
|
|
18221
18241
|
* Re-checks the count first (activity between the timer firing and now re-arms instead). Dropping the
|
|
18222
18242
|
* entry here and the session's own `close` → {@link remove} are both idempotent.
|
|
18223
18243
|
*/
|
|
18224
|
-
onIdle(
|
|
18225
|
-
const e = this.entries.get(
|
|
18244
|
+
onIdle(key) {
|
|
18245
|
+
const e = this.entries.get(key);
|
|
18226
18246
|
if (!e)
|
|
18227
18247
|
return;
|
|
18228
18248
|
if (e.retained > 0)
|
|
18229
18249
|
return;
|
|
18230
|
-
this.logger.debug(`[session ${
|
|
18231
|
-
void this.autoClose(
|
|
18250
|
+
this.logger.debug(`[session ${key}] idle window elapsed \u2014 disconnecting now (device can sleep)`);
|
|
18251
|
+
void this.autoClose(key).catch((error) => this.logger.error(`[session ${key}] idle detach failed`, error));
|
|
18232
18252
|
}
|
|
18233
18253
|
/**
|
|
18234
18254
|
* Close a station the manager itself decided to close, and announce it.
|
|
@@ -18246,23 +18266,23 @@ var SessionManager = class {
|
|
|
18246
18266
|
* An entry that never carried a session is still torn down, but silently: a pre-warm whose open failed
|
|
18247
18267
|
* leaves one behind, and announcing it would report a station closed that was never reported open.
|
|
18248
18268
|
*/
|
|
18249
|
-
async autoClose(
|
|
18250
|
-
const entry = this.discard(
|
|
18269
|
+
async autoClose(key) {
|
|
18270
|
+
const entry = this.discard(key);
|
|
18251
18271
|
if (!entry)
|
|
18252
18272
|
return;
|
|
18253
18273
|
if (entry.session)
|
|
18254
|
-
this.opts.onAutoClose?.(
|
|
18274
|
+
this.opts.onAutoClose?.(key);
|
|
18255
18275
|
await this.closeEntry(entry);
|
|
18256
18276
|
}
|
|
18257
18277
|
/** Drop a station's entry + timer (called from the session's `close` handler). Idempotent. */
|
|
18258
|
-
remove(
|
|
18259
|
-
const entry = this.discard(
|
|
18278
|
+
remove(key) {
|
|
18279
|
+
const entry = this.discard(key);
|
|
18260
18280
|
if (entry)
|
|
18261
18281
|
this.settleReset(entry);
|
|
18262
18282
|
}
|
|
18263
18283
|
/** Close one station now and discard its lifecycle entry. */
|
|
18264
|
-
async close(
|
|
18265
|
-
const entry = this.discard(
|
|
18284
|
+
async close(key) {
|
|
18285
|
+
const entry = this.discard(key);
|
|
18266
18286
|
if (entry)
|
|
18267
18287
|
await this.closeEntry(entry);
|
|
18268
18288
|
}
|
|
@@ -18276,12 +18296,12 @@ var SessionManager = class {
|
|
|
18276
18296
|
* Both branches close through {@link autoClose}: the caller asked for a recycle, not for the station's
|
|
18277
18297
|
* live sources to be dropped, so it does not clean up after one — exactly like the idle path.
|
|
18278
18298
|
*/
|
|
18279
|
-
resetWhenUnused(
|
|
18280
|
-
const entry = this.entries.get(
|
|
18299
|
+
resetWhenUnused(key) {
|
|
18300
|
+
const entry = this.entries.get(key);
|
|
18281
18301
|
if (!entry)
|
|
18282
18302
|
return Promise.resolve();
|
|
18283
18303
|
if (entry.retained <= entry.holdTimers.size)
|
|
18284
|
-
return this.autoClose(
|
|
18304
|
+
return this.autoClose(key);
|
|
18285
18305
|
entry.reset ??= Promise.withResolvers();
|
|
18286
18306
|
return entry.reset.promise;
|
|
18287
18307
|
}
|
|
@@ -18313,22 +18333,22 @@ var SessionManager = class {
|
|
|
18313
18333
|
* discarded entry owns no live timer, which is what stops a deferred release from landing on whatever
|
|
18314
18334
|
* entry next occupies this serial.
|
|
18315
18335
|
*/
|
|
18316
|
-
discard(
|
|
18317
|
-
const entry = this.entries.get(
|
|
18336
|
+
discard(key) {
|
|
18337
|
+
const entry = this.entries.get(key);
|
|
18318
18338
|
if (!entry)
|
|
18319
18339
|
return void 0;
|
|
18320
18340
|
entry.idle.cancel();
|
|
18321
18341
|
for (const timer of entry.holdTimers)
|
|
18322
18342
|
clearTimeout(timer);
|
|
18323
18343
|
entry.holdTimers.clear();
|
|
18324
|
-
this.entries.delete(
|
|
18344
|
+
this.entries.delete(key);
|
|
18325
18345
|
return entry;
|
|
18326
18346
|
}
|
|
18327
18347
|
/** Close every session and clear all timers. */
|
|
18328
18348
|
async closeAll() {
|
|
18329
18349
|
this.generation++;
|
|
18330
|
-
const entries = [...this.entries.keys()].flatMap((
|
|
18331
|
-
const entry = this.discard(
|
|
18350
|
+
const entries = [...this.entries.keys()].flatMap((key) => {
|
|
18351
|
+
const entry = this.discard(key);
|
|
18332
18352
|
return entry ? [entry] : [];
|
|
18333
18353
|
});
|
|
18334
18354
|
const results = await Promise.allSettled(entries.map((entry) => this.closeEntry(entry)));
|
|
@@ -19354,7 +19374,7 @@ function sameLiveOpt(a, b) {
|
|
|
19354
19374
|
}
|
|
19355
19375
|
var P2PCommandRouter = class _P2PCommandRouter {
|
|
19356
19376
|
deps;
|
|
19357
|
-
/**
|
|
19377
|
+
/** P2P session lifecycle: on-demand open + battery-aware idle-detach + refcount, per session key. */
|
|
19358
19378
|
manager;
|
|
19359
19379
|
/** Error objects already forwarded while a station startup awaits the same session signal. */
|
|
19360
19380
|
reportedErrors = /* @__PURE__ */ new WeakSet();
|
|
@@ -19362,6 +19382,21 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19362
19382
|
liveSources = /* @__PURE__ */ new Map();
|
|
19363
19383
|
/** The options each live source was built from, so a later caller's conflicting ones can be reported. */
|
|
19364
19384
|
liveSourceOpts = /* @__PURE__ */ new Map();
|
|
19385
|
+
/**
|
|
19386
|
+
* The session each live source pulls over — the station's own serial, or the source's own media
|
|
19387
|
+
* session key.
|
|
19388
|
+
*
|
|
19389
|
+
* Written the instant the choice is made and BEFORE the connection is opened, which is what makes it
|
|
19390
|
+
* a reservation rather than a record. Two cameras started together (the four-tile case this feature
|
|
19391
|
+
* exists for) would otherwise both find the station's session free: a consumer attaches only after
|
|
19392
|
+
* `sharedLiveSourceFor` returns, so neither is visible to the other through consumer counts, and both
|
|
19393
|
+
* would take the shared connection and contend on it.
|
|
19394
|
+
*
|
|
19395
|
+
* A station entry is kept as well as a media one, because "is the station's own session already
|
|
19396
|
+
* claimed" is the question being asked, and an entry that is present but not yet in
|
|
19397
|
+
* {@link liveSources} is a claim in flight.
|
|
19398
|
+
*/
|
|
19399
|
+
liveSessionKeys = /* @__PURE__ */ new Map();
|
|
19365
19400
|
/**
|
|
19366
19401
|
* The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
|
|
19367
19402
|
* time and the session carries one audio sequence, so this path is exclusive where a live pull is
|
|
@@ -19375,7 +19410,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19375
19410
|
this.manager = new SessionManager({
|
|
19376
19411
|
poweredFor: deps.poweredFor,
|
|
19377
19412
|
logger: deps.logger,
|
|
19378
|
-
onAutoClose: (
|
|
19413
|
+
onAutoClose: (key) => _P2PCommandRouter.isMediaSessionKey(key) ? this.tearDownMediaSession(key) : this.tearDownStation(key),
|
|
19379
19414
|
...deps.sessionIdle
|
|
19380
19415
|
});
|
|
19381
19416
|
}
|
|
@@ -19407,7 +19442,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19407
19442
|
static claimsDevice(dev) {
|
|
19408
19443
|
return typeof dev.p2pDid === "string" && dev.p2pDid.length > 0;
|
|
19409
19444
|
}
|
|
19410
|
-
/**
|
|
19445
|
+
/**
|
|
19446
|
+
* The open P2P sessions by key — a station's own under its serial, a camera's media session under
|
|
19447
|
+
* `<stationSn>#live:<channel>` (a snapshot; mutate via the lifecycle methods, not this map).
|
|
19448
|
+
*/
|
|
19411
19449
|
getSessions() {
|
|
19412
19450
|
return this.manager.liveSessions();
|
|
19413
19451
|
}
|
|
@@ -19419,16 +19457,16 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19419
19457
|
*
|
|
19420
19458
|
* One hold, taken before the open so a slow connect can't idle-close mid-flight. It expires on its
|
|
19421
19459
|
* own, which arms the station's idle window rather than closing the session, per {@link PREWARM_MS}.
|
|
19422
|
-
* A second hold after the open would buy nothing: {@link
|
|
19460
|
+
* A second hold after the open would buy nothing: {@link openSession} returns once the socket is bound
|
|
19423
19461
|
* and the lookups are away, not once the peer has answered, so both would expire together.
|
|
19424
19462
|
*
|
|
19425
19463
|
* Best-effort — a failed open surfaces via `onError`. A {@link SessionSupersededError} does not: the
|
|
19426
19464
|
* session was deliberately closed underneath a speculative open, which is not a fault to report.
|
|
19427
19465
|
*/
|
|
19428
19466
|
async prewarm(parentSn, ms = PREWARM_MS) {
|
|
19429
|
-
this.manager.hold(parentSn, ms);
|
|
19467
|
+
this.manager.hold(parentSn, ms, parentSn);
|
|
19430
19468
|
try {
|
|
19431
|
-
await this.
|
|
19469
|
+
await this.openSession(parentSn, parentSn);
|
|
19432
19470
|
} catch (e) {
|
|
19433
19471
|
if (e instanceof SessionSupersededError)
|
|
19434
19472
|
return;
|
|
@@ -19445,6 +19483,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19445
19483
|
src.dispose();
|
|
19446
19484
|
this.liveSources.clear();
|
|
19447
19485
|
this.liveSourceOpts.clear();
|
|
19486
|
+
this.liveSessionKeys.clear();
|
|
19448
19487
|
await this.manager.closeAll();
|
|
19449
19488
|
}
|
|
19450
19489
|
/** This serial's loaded record, or `undefined` — the one place the cached list is searched by serial. */
|
|
@@ -19486,8 +19525,55 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19486
19525
|
* when present, else the freshest private IP in the record ({@link freshestLanIp}) — so P2P works
|
|
19487
19526
|
* on-LAN even when broadcast is blocked (AP isolation) or the record's `ip_addr` went stale.
|
|
19488
19527
|
*/
|
|
19489
|
-
|
|
19490
|
-
|
|
19528
|
+
/**
|
|
19529
|
+
* The key a camera's own media session is filed under, distinct from every station serial because a
|
|
19530
|
+
* serial contains no `#`.
|
|
19531
|
+
*/
|
|
19532
|
+
static mediaSessionKey(parentSn, channel) {
|
|
19533
|
+
return `${parentSn}#live:${channel}`;
|
|
19534
|
+
}
|
|
19535
|
+
/** Whether `key` names a media session rather than a station's own. */
|
|
19536
|
+
static isMediaSessionKey(key) {
|
|
19537
|
+
return key.includes("#live:");
|
|
19538
|
+
}
|
|
19539
|
+
/**
|
|
19540
|
+
* Open (or reuse) a SECOND connection to a station, carrying one camera's media and nothing else.
|
|
19541
|
+
*
|
|
19542
|
+
* One session serves one camera: a station fans its cameras over a session and answers the most recent
|
|
19543
|
+
* start on it, so two cameras down one tunnel take it from each other in turn. Another connection is
|
|
19544
|
+
* how a station serves another camera.
|
|
19545
|
+
*
|
|
19546
|
+
* The hardware was shown to do this before the SDK did. A first-party display showing four tiles was
|
|
19547
|
+
* captured opening one PPCS session per camera, with three cameras' video arriving in the same second
|
|
19548
|
+
* at 2304x1296, 1600x1200 and 3840x2160 — three geometries at once, which no composed stream can be.
|
|
19549
|
+
* Reproduced here afterwards on a base carrying two attached cameras, one at 3840x2160, both holding
|
|
19550
|
+
* full frame rate at once over a session each, where the same pair down one session could only take
|
|
19551
|
+
* turns.
|
|
19552
|
+
*
|
|
19553
|
+
* It carries media alone. The station announces its state to every client that connects, so a session
|
|
19554
|
+
* wired to the same fan-out would report every event a second time; {@link makeSession} leaves this one
|
|
19555
|
+
* unannounced, and the station's own session stays the single source of connection state, control
|
|
19556
|
+
* notifications and frames.
|
|
19557
|
+
*
|
|
19558
|
+
* The level-2 key is waited for here on the same terms the station's own session gets, because a
|
|
19559
|
+
* connection is not usable for this without one: an attached camera's media start has no level-1 form,
|
|
19560
|
+
* so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
|
|
19561
|
+
* shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
|
|
19562
|
+
* carry the start is not a connected session, so this refuses rather than returning one.
|
|
19563
|
+
*/
|
|
19564
|
+
async openMediaSession(parentSn, channel, signal) {
|
|
19565
|
+
const session = await this.openSession(_P2PCommandRouter.mediaSessionKey(parentSn, channel), parentSn);
|
|
19566
|
+
let ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
19567
|
+
if (!ready && session.repromptLevel2Key()) {
|
|
19568
|
+
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
19569
|
+
}
|
|
19570
|
+
if (!ready)
|
|
19571
|
+
throw new StationKeyUnavailableError(parentSn);
|
|
19572
|
+
return session;
|
|
19573
|
+
}
|
|
19574
|
+
/** Open (or reuse) the session filed under `key`, dialling `parentSn`'s endpoint. */
|
|
19575
|
+
async openSession(key, parentSn) {
|
|
19576
|
+
return this.manager.acquire(key, async (register) => {
|
|
19491
19577
|
const stationDev = this.recordFor(parentSn);
|
|
19492
19578
|
if (!stationDev)
|
|
19493
19579
|
throw new Error(`station ${parentSn} is not in the device list`);
|
|
@@ -19502,11 +19588,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19502
19588
|
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
19503
19589
|
}
|
|
19504
19590
|
const localAddress = this.deps.localAddresses?.[parentSn] ?? freshestLanIp(raw);
|
|
19505
|
-
const session = this.makeSession(parentSn, did, raw, dskKey, localAddress);
|
|
19591
|
+
const session = this.makeSession(parentSn, did, raw, dskKey, localAddress, key, key === parentSn);
|
|
19506
19592
|
register(session);
|
|
19507
19593
|
await session.connect();
|
|
19508
19594
|
return session;
|
|
19509
|
-
});
|
|
19595
|
+
}, parentSn);
|
|
19510
19596
|
}
|
|
19511
19597
|
/**
|
|
19512
19598
|
* Build + wire a {@link P2PSession} for a station (NOT yet connected — the caller awaits `connect()`).
|
|
@@ -19519,8 +19605,14 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19519
19605
|
*
|
|
19520
19606
|
* The `close` handler drops the session from the {@link SessionManager} and disposes any shared live
|
|
19521
19607
|
* source riding this station (consumers get `stop`; a later attach rebuilds via the factory).
|
|
19608
|
+
*
|
|
19609
|
+
* A session filed under a media key is wired for errors and its own teardown ONLY. Connection state,
|
|
19610
|
+
* level-2 readiness and inbound frames all reach the owner through the station's own session, and a
|
|
19611
|
+
* station announces those to every client that connects — so fanning a second connection's copies out
|
|
19612
|
+
* under the same station serial would report each one twice, and a close would tear down the station
|
|
19613
|
+
* while its own session is still serving.
|
|
19522
19614
|
*/
|
|
19523
|
-
makeSession(stationSn, did, raw, dskKey, localAddress) {
|
|
19615
|
+
makeSession(stationSn, did, raw, dskKey, localAddress, key, announces) {
|
|
19524
19616
|
const conn = raw?.p2p_conn ?? raw?.app_conn;
|
|
19525
19617
|
const adminUserId = raw?.member?.admin_user_id || this.deps.mega.auth?.userId || "";
|
|
19526
19618
|
const session = new P2PSession({
|
|
@@ -19554,6 +19646,14 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19554
19646
|
},
|
|
19555
19647
|
logger: this.deps.logger ?? noopLogger
|
|
19556
19648
|
});
|
|
19649
|
+
session.on("error", (e) => this.reportError(e));
|
|
19650
|
+
if (!announces) {
|
|
19651
|
+
session.on("close", () => {
|
|
19652
|
+
if (this.manager.get(key) === session)
|
|
19653
|
+
this.tearDownMediaSession(key);
|
|
19654
|
+
});
|
|
19655
|
+
return session;
|
|
19656
|
+
}
|
|
19557
19657
|
session.on("connect", () => {
|
|
19558
19658
|
if (this.manager.get(stationSn) === session)
|
|
19559
19659
|
this.deps.onConnect(stationSn);
|
|
@@ -19563,7 +19663,6 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19563
19663
|
return;
|
|
19564
19664
|
this.tearDownStation(stationSn);
|
|
19565
19665
|
});
|
|
19566
|
-
session.on("error", (e) => this.reportError(e));
|
|
19567
19666
|
session.on("level2Ready", ({ cipherId }) => this.deps.onLevel2Ready(stationSn, cipherId));
|
|
19568
19667
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
19569
19668
|
return session;
|
|
@@ -19574,7 +19673,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19574
19673
|
*/
|
|
19575
19674
|
async ensureStation(parentSn, signal) {
|
|
19576
19675
|
try {
|
|
19577
|
-
const session = await this.
|
|
19676
|
+
const session = await this.openSession(parentSn, parentSn);
|
|
19578
19677
|
if (session.isConnected)
|
|
19579
19678
|
return;
|
|
19580
19679
|
await new Promise((resolve, reject) => {
|
|
@@ -19622,7 +19721,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19622
19721
|
const dev = this.recordFor(sn);
|
|
19623
19722
|
if (!dev)
|
|
19624
19723
|
throw new Error(`device ${sn} not found`);
|
|
19625
|
-
await this.
|
|
19724
|
+
await this.openSession(this.stationKeyFor(dev), this.stationKeyFor(dev));
|
|
19626
19725
|
return dev;
|
|
19627
19726
|
}
|
|
19628
19727
|
/**
|
|
@@ -19706,11 +19805,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19706
19805
|
});
|
|
19707
19806
|
},
|
|
19708
19807
|
live: async (opts) => {
|
|
19709
|
-
const source = await this.sharedLiveSourceFor(sn, opts);
|
|
19808
|
+
const source = await this.sharedLiveSourceFor(sn, opts, true);
|
|
19710
19809
|
return this.attachUnlessAborted(source, opts?.signal);
|
|
19711
19810
|
},
|
|
19712
19811
|
openReadable: async (opts) => {
|
|
19713
|
-
const source = await this.sharedLiveSourceFor(sn, opts ?? {});
|
|
19812
|
+
const source = await this.sharedLiveSourceFor(sn, opts ?? {}, true);
|
|
19714
19813
|
return openReadableFromConsumer(this.attachUnlessAborted(source, opts?.signal), opts);
|
|
19715
19814
|
},
|
|
19716
19815
|
recordFragments: (opts) => this.recordFragments(sn, opts),
|
|
@@ -19851,10 +19950,17 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19851
19950
|
* {@link releaseLingeringSiblings}. A pull with consumers is never touched. The release runs before the
|
|
19852
19951
|
* reuse branch, so a reuse frees the station as a cold start does.
|
|
19853
19952
|
*
|
|
19953
|
+
* `mayOpenOwnSession` decides whether a camera that finds the station's own session claimed may open a
|
|
19954
|
+
* connection of its own for it. Only the continuous-pull egresses pass it. A still must not: it wants one
|
|
19955
|
+
* frame, and a socket plus a level-2 negotiation per thumbnail is a cost a tile refresh cannot justify —
|
|
19956
|
+
* so a still asked for while a sibling is being watched contends as it always did, and the caller's
|
|
19957
|
+
* retained image answers it. A live view still outranks a tile; what changed is that two live views no
|
|
19958
|
+
* longer have to outrank each other.
|
|
19959
|
+
*
|
|
19854
19960
|
* The session goes into a {@link HeldSession} cell, so it can be replaced under a source that stays in
|
|
19855
19961
|
* place.
|
|
19856
19962
|
*/
|
|
19857
|
-
async sharedLiveSourceFor(sn, opts = {}) {
|
|
19963
|
+
async sharedLiveSourceFor(sn, opts = {}, mayOpenOwnSession = false) {
|
|
19858
19964
|
const { session, parentSn, channel, accountId, homeBaseAttached } = await this.resolveSession(sn, {
|
|
19859
19965
|
waitLevel2: "soft",
|
|
19860
19966
|
requireLevel2ForAttached: true,
|
|
@@ -19867,14 +19973,19 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19867
19973
|
source = void 0;
|
|
19868
19974
|
}
|
|
19869
19975
|
this.releaseLingeringSiblings(parentSn, channel);
|
|
19870
|
-
if (homeBaseAttached) {
|
|
19871
|
-
const serving = this.occupiedSiblingChannel(parentSn, key);
|
|
19872
|
-
if (serving !== void 0)
|
|
19873
|
-
throw new StationBusyError(serving);
|
|
19874
|
-
}
|
|
19875
19976
|
if (!source) {
|
|
19876
19977
|
const logger = this.deps.logger ?? noopLogger;
|
|
19877
|
-
const
|
|
19978
|
+
const sessionKey = mayOpenOwnSession && homeBaseAttached && this.stationSessionInUse(parentSn, key) ? _P2PCommandRouter.mediaSessionKey(parentSn, channel) : parentSn;
|
|
19979
|
+
this.liveSessionKeys.set(key, sessionKey);
|
|
19980
|
+
let held;
|
|
19981
|
+
try {
|
|
19982
|
+
held = {
|
|
19983
|
+
session: sessionKey === parentSn ? session : await this.openMediaSession(parentSn, channel, opts.signal)
|
|
19984
|
+
};
|
|
19985
|
+
} catch (error) {
|
|
19986
|
+
this.liveSessionKeys.delete(key);
|
|
19987
|
+
throw error;
|
|
19988
|
+
}
|
|
19878
19989
|
source = new SharedLiveSource({
|
|
19879
19990
|
makeStream: (ctx) => new LiveStream(held.session, {
|
|
19880
19991
|
channel,
|
|
@@ -19892,8 +20003,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19892
20003
|
budgetGraceMs: opts.budgetGraceMs,
|
|
19893
20004
|
logger,
|
|
19894
20005
|
label: key,
|
|
19895
|
-
onActive: () => this.manager.retain(
|
|
19896
|
-
onIdle: () => this.manager.release(
|
|
20006
|
+
onActive: () => this.manager.retain(sessionKey),
|
|
20007
|
+
onIdle: () => this.manager.release(sessionKey),
|
|
20008
|
+
onStopped: () => this.closeMediaSession(key),
|
|
19897
20009
|
onStartFailed: () => this.onLiveStartFailed(sn, key),
|
|
19898
20010
|
onSessionUnreachable: () => this.replaceUnreachableSession(sn, key, held)
|
|
19899
20011
|
});
|
|
@@ -19908,14 +20020,25 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19908
20020
|
* Tear down any pull on this station that is lingering for ANOTHER camera, before starting this one.
|
|
19909
20021
|
*
|
|
19910
20022
|
* A lingering pull has no consumers but is still held open, and on an attached camera holding it open means
|
|
19911
|
-
* re-sending the full media start every keepalive tick. Two channels doing that at once
|
|
19912
|
-
* serves one camera at a time leaves the new stream receiving nothing but the old camera's frames
|
|
19913
|
-
* long as the linger lasts.
|
|
20023
|
+
* re-sending the full media start every keepalive tick. Two channels doing that at once over ONE session,
|
|
20024
|
+
* which serves one camera at a time, leaves the new stream receiving nothing but the old camera's frames
|
|
20025
|
+
* for as long as the linger lasts.
|
|
19914
20026
|
*
|
|
19915
20027
|
* Several cameras genuinely being WATCHED together are never disturbed — the linger exists to make
|
|
19916
20028
|
* re-opening the SAME camera cheap, and it keeps doing that. What it may not do is keep a camera nobody is
|
|
19917
20029
|
* looking at competing with one somebody just asked for.
|
|
19918
20030
|
*
|
|
20031
|
+
* An `idle` source is skipped, because it is not a linger and holds nothing: it has never warmed, so it
|
|
20032
|
+
* has sent no media start and is competing for nothing. Without that, two cameras opened at the same
|
|
20033
|
+
* moment destroy each other — the second finds the first's source built but not yet attached to, reads
|
|
20034
|
+
* zero consumers as a linger, and disposes the source its caller is holding. Which is the four-tile case
|
|
20035
|
+
* this whole path exists for.
|
|
20036
|
+
*
|
|
20037
|
+
* A sibling lingering on a connection of its OWN is skipped for the same reason stated the other way: the
|
|
20038
|
+
* contention this releases is contention over one session, and that sibling is not on this one. Dropping
|
|
20039
|
+
* it would close a socket and throw away the cheap re-attach the linger exists to provide, to relieve a
|
|
20040
|
+
* competition that is not happening.
|
|
20041
|
+
*
|
|
19919
20042
|
* A snapshot tile is nobody looking. Opening a live view in the Home app takes that cell fullscreen, so the
|
|
19920
20043
|
* pulls refreshing the other cells are off screen, yet each goes on re-issuing its own media start every
|
|
19921
20044
|
* retry tick — measured as four pulls warming together off one HomeBase, a live request landing 1.4 s later,
|
|
@@ -19929,28 +20052,49 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19929
20052
|
for (const [key, source] of [...this.liveSources]) {
|
|
19930
20053
|
if (!key.startsWith(`${parentSn}:`) || key === own || source.consumerCount > 0)
|
|
19931
20054
|
continue;
|
|
19932
|
-
(
|
|
20055
|
+
if (source.state === "idle")
|
|
20056
|
+
continue;
|
|
20057
|
+
if (this.liveSessionKeys.get(key) !== parentSn)
|
|
20058
|
+
continue;
|
|
20059
|
+
(this.deps.logger ?? noopLogger).debug(`[live ${key}] releasing a pull nothing is attached to so ${own} can start \u2014 one session serves one camera at a time`);
|
|
19933
20060
|
this.dropLiveSource(key);
|
|
19934
20061
|
}
|
|
19935
20062
|
}
|
|
19936
20063
|
/**
|
|
19937
|
-
*
|
|
20064
|
+
* Whether another camera has already claimed this station's OWN session, ignoring `key`.
|
|
20065
|
+
*
|
|
20066
|
+
* The question a newcomer has to answer is not whether the station is busy — it can serve one camera
|
|
20067
|
+
* per connection — but whether the connection it would otherwise share is taken. A camera on a media
|
|
20068
|
+
* session of its own does not hold this one, so a station whose first camera has since stopped hands
|
|
20069
|
+
* its own session to the next arrival rather than opening a socket beside an idle one.
|
|
20070
|
+
*
|
|
20071
|
+
* A claim counts while its source is still `idle`, source or no source. That is a start that has been
|
|
20072
|
+
* handed to its caller but not yet attached to, and it is the only state in which two cameras asked for
|
|
20073
|
+
* at the same moment can see each other: consumers attach after this method has already run for both.
|
|
20074
|
+
*
|
|
20075
|
+
* The cost is that a pull genuinely abandoned before its first attach goes on holding the station's own
|
|
20076
|
+
* session, and the next camera pays for a connection of its own rather than reclaiming it. That is one
|
|
20077
|
+
* socket against destroying a start someone is waiting on, which is not a close trade.
|
|
19938
20078
|
*
|
|
19939
20079
|
* A stopped source is skipped even when consumers are still attached to it. A failed start fails its
|
|
19940
20080
|
* consumers without detaching them, so a caller still holding a dead handle leaves the count non-zero,
|
|
19941
|
-
* and counting that as a viewer would
|
|
19942
|
-
* restarted. Only a source that can still deliver holds a place.
|
|
20081
|
+
* and counting that as a viewer would put every later camera on a connection of its own until the
|
|
20082
|
+
* client restarted. Only a source that can still deliver holds a place.
|
|
19943
20083
|
*/
|
|
19944
|
-
|
|
19945
|
-
for (const [siblingKey,
|
|
19946
|
-
if (siblingKey === key ||
|
|
20084
|
+
stationSessionInUse(parentSn, key) {
|
|
20085
|
+
for (const [siblingKey, sessionKey] of this.liveSessionKeys) {
|
|
20086
|
+
if (siblingKey === key || sessionKey !== parentSn)
|
|
19947
20087
|
continue;
|
|
19948
|
-
|
|
20088
|
+
const sibling = this.liveSources.get(siblingKey);
|
|
20089
|
+
if (!sibling)
|
|
20090
|
+
return true;
|
|
20091
|
+
if (sibling.state === "stopped")
|
|
19949
20092
|
continue;
|
|
19950
|
-
|
|
19951
|
-
|
|
20093
|
+
if (sibling.state !== "idle" && sibling.consumerCount === 0)
|
|
20094
|
+
continue;
|
|
20095
|
+
return true;
|
|
19952
20096
|
}
|
|
19953
|
-
return
|
|
20097
|
+
return false;
|
|
19954
20098
|
}
|
|
19955
20099
|
/**
|
|
19956
20100
|
* Whether the stream on `key` should re-assert its channel to hold the station.
|
|
@@ -19961,8 +20105,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19961
20105
|
* - Nothing attached: no. There is nobody to take the station for.
|
|
19962
20106
|
* - A live viewer attached: yes. That is the picture someone is looking at.
|
|
19963
20107
|
* - Held only for stills, while a sibling on this station has a live viewer: no. A still refreshes a
|
|
19964
|
-
* tile that is off screen while the live view is on it, and
|
|
19965
|
-
* cannot satisfy both. Measured: a still on a sibling halved a live view's frame rate for as long as
|
|
20108
|
+
* tile that is off screen while the live view is on it, and one session serving one camera at a time
|
|
20109
|
+
* 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
|
|
19966
20110
|
* it took, and its own capture then took fifteen seconds because it was contending.
|
|
19967
20111
|
*
|
|
19968
20112
|
* A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
|
|
@@ -19984,7 +20128,13 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19984
20128
|
}
|
|
19985
20129
|
return consumer;
|
|
19986
20130
|
}
|
|
19987
|
-
/**
|
|
20131
|
+
/**
|
|
20132
|
+
* Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
|
|
20133
|
+
* on a session goes with it, and a media session opened for this source alone is closed: nothing else
|
|
20134
|
+
* can reach that connection, so leaving it open would hold a socket and a station keepalive for a
|
|
20135
|
+
* camera no longer being pulled. A claim on the STATION's own session is released without closing
|
|
20136
|
+
* anything — that connection carries the station's control traffic and outlives any one camera.
|
|
20137
|
+
*/
|
|
19988
20138
|
dropLiveSource(key) {
|
|
19989
20139
|
const source = this.liveSources.get(key);
|
|
19990
20140
|
if (!source)
|
|
@@ -19992,6 +20142,34 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
19992
20142
|
source.dispose();
|
|
19993
20143
|
this.liveSources.delete(key);
|
|
19994
20144
|
this.liveSourceOpts.delete(key);
|
|
20145
|
+
this.closeMediaSession(key);
|
|
20146
|
+
}
|
|
20147
|
+
/** Close and forget the media session `key`'s live source owned, if it owned one. */
|
|
20148
|
+
closeMediaSession(key) {
|
|
20149
|
+
const sessionKey = this.liveSessionKeys.get(key);
|
|
20150
|
+
if (sessionKey === void 0)
|
|
20151
|
+
return;
|
|
20152
|
+
this.liveSessionKeys.delete(key);
|
|
20153
|
+
if (!_P2PCommandRouter.isMediaSessionKey(sessionKey))
|
|
20154
|
+
return;
|
|
20155
|
+
void this.manager.close(sessionKey).catch((error) => this.reportError(error instanceof Error ? error : new Error(String(error))));
|
|
20156
|
+
}
|
|
20157
|
+
/**
|
|
20158
|
+
* Drop the live source a media session was carrying, after that session closed on its own.
|
|
20159
|
+
*
|
|
20160
|
+
* The source holds the closed connection and never re-resolves it, so it can only answer its retained
|
|
20161
|
+
* keyframe and then fail on its warm-up deadline. Its consumers get `stop`, and the next attach builds
|
|
20162
|
+
* a fresh source — which, finding the station busy again, opens a fresh media session for it.
|
|
20163
|
+
*/
|
|
20164
|
+
tearDownMediaSession(sessionKey) {
|
|
20165
|
+
this.manager.remove(sessionKey);
|
|
20166
|
+
for (const [key, owned] of this.liveSessionKeys) {
|
|
20167
|
+
if (owned === sessionKey) {
|
|
20168
|
+
this.liveSessionKeys.delete(key);
|
|
20169
|
+
this.dropLiveSource(key);
|
|
20170
|
+
return;
|
|
20171
|
+
}
|
|
20172
|
+
}
|
|
19995
20173
|
}
|
|
19996
20174
|
/**
|
|
19997
20175
|
* Drop everything that was riding a station's session, and report the station closed.
|
|
@@ -20335,14 +20513,15 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20335
20513
|
const parentSn = homeBaseAttached ? raw.parent_sn : dev.stationSn ?? sn;
|
|
20336
20514
|
const session = this.manager.get(parentSn) ?? this.manager.get(sn) ?? (dev.stationSn ? this.manager.get(dev.stationSn) : void 0);
|
|
20337
20515
|
if (!session) {
|
|
20338
|
-
|
|
20516
|
+
const stations = this.manager.keys().filter((k) => !_P2PCommandRouter.isMediaSessionKey(k));
|
|
20517
|
+
throw new Error(`no P2P session for ${sn} (known: ${stations.join(", ") || "none"})`);
|
|
20339
20518
|
}
|
|
20340
20519
|
if (session.pathAnswering === false && !rebuilt) {
|
|
20341
20520
|
(this.deps.logger ?? noopLogger).debug(`[p2p] ${parentSn} path stopped answering \u2014 rebuilding before use`);
|
|
20342
20521
|
await this.manager.close(parentSn).catch((error) => this.reportError(error instanceof Error ? error : new Error(String(error))));
|
|
20343
20522
|
return await this.resolveSession(sn, opts, true);
|
|
20344
20523
|
}
|
|
20345
|
-
this.manager.bumpCommand(parentSn);
|
|
20524
|
+
this.manager.bumpCommand(parentSn, parentSn);
|
|
20346
20525
|
const channel = typeof raw.device_channel === "number" ? raw.device_channel : 0;
|
|
20347
20526
|
const stationAdminId = raw.member?.admin_user_id;
|
|
20348
20527
|
const accountId = stationAdminId ?? this.deps.mega.auth?.userId ?? "";
|
|
@@ -24649,9 +24828,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
24649
24828
|
/**
|
|
24650
24829
|
* Combine explicit P2P media with the optional passive push-thumbnail provider.
|
|
24651
24830
|
*
|
|
24652
|
-
* The retained still also becomes the answer for a live still that could not be captured.
|
|
24653
|
-
* serves one camera at a time and a live view outranks a tile
|
|
24654
|
-
* being watched is refused at the transport. Answering the retained bytes answers the read rather than
|
|
24831
|
+
* The retained still also becomes the answer for a live still that could not be captured. One session
|
|
24832
|
+
* serves one camera at a time and a live view outranks a tile — a still does not open a connection of its
|
|
24833
|
+
* own — so a still asked for while a sibling is being watched is refused at the transport. Answering the retained bytes answers the read rather than
|
|
24655
24834
|
* failing it, marked {@link MediaProvider.snapshotLive} `retained` so the caller knows they are not
|
|
24656
24835
|
* current. With nothing retained the refusal stands.
|
|
24657
24836
|
*/
|
|
@@ -25404,10 +25583,15 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25404
25583
|
await sink.dispatch(cmd);
|
|
25405
25584
|
}
|
|
25406
25585
|
/**
|
|
25407
|
-
*
|
|
25586
|
+
* The open P2P sessions, by key. P2P is auto-managed: wired stations are warmed at login, battery
|
|
25408
25587
|
* stations open on demand (command / stream, or an opted-in event pre-warm) and idle-detach — so this
|
|
25409
|
-
* map grows and shrinks over time.
|
|
25410
|
-
*
|
|
25588
|
+
* map grows and shrinks over time.
|
|
25589
|
+
*
|
|
25590
|
+
* A station's own session is keyed by its serial, and `p2pConnect(stationSn)` / `p2pClose(stationSn)`
|
|
25591
|
+
* track those. A station serving more than one camera at once also holds a session per extra camera,
|
|
25592
|
+
* keyed `<stationSn>#live:<channel>` — these carry media alone and raise no connection events, because
|
|
25593
|
+
* a station announces its state to every client that connects and reporting each copy would double
|
|
25594
|
+
* every event the station's own session already delivers.
|
|
25411
25595
|
*/
|
|
25412
25596
|
getP2pSessions() {
|
|
25413
25597
|
return this.p2p.getSessions();
|
|
@@ -26480,7 +26664,6 @@ export {
|
|
|
26480
26664
|
SolixDevice,
|
|
26481
26665
|
SolixMqtt,
|
|
26482
26666
|
StateConvergenceError,
|
|
26483
|
-
StationBusyError,
|
|
26484
26667
|
StationKeyUnavailableError,
|
|
26485
26668
|
StationUnreachableError,
|
|
26486
26669
|
StoredSnapshotUnavailableError,
|