@apocaliss92/nodelink-js 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{chunk-UHFJPQA4.js → chunk-WDFKIHM5.js} +166 -16
- package/dist/chunk-WDFKIHM5.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +165 -15
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +165 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-UHFJPQA4.js.map +0 -1
package/dist/cli/rtsp-server.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -9682,6 +9682,14 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
9682
9682
|
* even if the current client instance is idle/disconnected.
|
|
9683
9683
|
*/
|
|
9684
9684
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
9685
|
+
/**
|
|
9686
|
+
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
9687
|
+
*
|
|
9688
|
+
* Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
|
|
9689
|
+
* and creates a new one. Instance-level backoff variables would reset to zero,
|
|
9690
|
+
* allowing immediate reconnection and perpetuating the storm.
|
|
9691
|
+
*/
|
|
9692
|
+
static d2cDiscBackoff = /* @__PURE__ */ new Map();
|
|
9685
9693
|
/**
|
|
9686
9694
|
* Global (process-wide) CoverPreview serialization.
|
|
9687
9695
|
*
|
|
@@ -10377,7 +10385,12 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10377
10385
|
}
|
|
10378
10386
|
async waitForUdpReconnectCooldown() {
|
|
10379
10387
|
const now = Date.now();
|
|
10380
|
-
const
|
|
10388
|
+
const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
|
|
10389
|
+
const effectiveCooldownUntil = Math.max(
|
|
10390
|
+
this.udpReconnectCooldownUntilMs,
|
|
10391
|
+
staticEntry?.cooldownUntilMs ?? 0
|
|
10392
|
+
);
|
|
10393
|
+
const waitMs = effectiveCooldownUntil - now;
|
|
10381
10394
|
if (waitMs <= 0) return;
|
|
10382
10395
|
const sid = this.socketSessionId;
|
|
10383
10396
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -10386,7 +10399,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10386
10399
|
host: this.opts.host,
|
|
10387
10400
|
sid,
|
|
10388
10401
|
uid: shortUid,
|
|
10389
|
-
waitMs
|
|
10402
|
+
waitMs,
|
|
10403
|
+
persistent: staticEntry != null
|
|
10390
10404
|
});
|
|
10391
10405
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
10392
10406
|
}
|
|
@@ -10629,21 +10643,30 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10629
10643
|
uid: shortUid2,
|
|
10630
10644
|
message: err.message
|
|
10631
10645
|
});
|
|
10632
|
-
const
|
|
10646
|
+
const hostKey = this.opts.host;
|
|
10647
|
+
const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
|
|
10648
|
+
const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
|
|
10633
10649
|
const baseMs = 2e3;
|
|
10634
10650
|
const maxMs = 3e4;
|
|
10635
10651
|
const nextBackoffMs = withinWindow ? Math.min(
|
|
10636
10652
|
maxMs,
|
|
10637
10653
|
Math.max(
|
|
10638
10654
|
baseMs,
|
|
10639
|
-
|
|
10655
|
+
prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
|
|
10640
10656
|
)
|
|
10641
10657
|
) : baseMs;
|
|
10642
|
-
|
|
10643
|
-
|
|
10658
|
+
const cooldownUntilMs = Math.max(
|
|
10659
|
+
prev?.cooldownUntilMs ?? 0,
|
|
10660
|
+
now + nextBackoffMs
|
|
10661
|
+
);
|
|
10662
|
+
_BaichuanClient.d2cDiscBackoff.set(hostKey, {
|
|
10663
|
+
backoffMs: nextBackoffMs,
|
|
10664
|
+
lastAtMs: now,
|
|
10665
|
+
cooldownUntilMs
|
|
10666
|
+
});
|
|
10644
10667
|
this.udpReconnectCooldownUntilMs = Math.max(
|
|
10645
10668
|
this.udpReconnectCooldownUntilMs,
|
|
10646
|
-
|
|
10669
|
+
cooldownUntilMs
|
|
10647
10670
|
);
|
|
10648
10671
|
this.logDebug("d2c_disc_backoff", {
|
|
10649
10672
|
transport: "udp",
|
|
@@ -10651,7 +10674,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10651
10674
|
sid: sid2,
|
|
10652
10675
|
uid: shortUid2,
|
|
10653
10676
|
backoffMs: nextBackoffMs,
|
|
10654
|
-
cooldownUntilMs
|
|
10677
|
+
cooldownUntilMs,
|
|
10678
|
+
persistent: true
|
|
10655
10679
|
});
|
|
10656
10680
|
this.stopKeepAlive();
|
|
10657
10681
|
this.loggedIn = false;
|
|
@@ -10660,6 +10684,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10660
10684
|
this.videoSubscriptions.clear();
|
|
10661
10685
|
this.recomputeGlobalStreamingContribution();
|
|
10662
10686
|
}
|
|
10687
|
+
this.emit("d2c_disc", { host: this.opts.host, atMs: now });
|
|
10663
10688
|
}
|
|
10664
10689
|
this.emit("error", err);
|
|
10665
10690
|
});
|
|
@@ -10958,6 +10983,13 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10958
10983
|
}
|
|
10959
10984
|
}
|
|
10960
10985
|
this.emit("push", frame);
|
|
10986
|
+
if (frame.header.cmdId === 252 && frame.body.length > 0) {
|
|
10987
|
+
try {
|
|
10988
|
+
this.emit("batteryPush", frame);
|
|
10989
|
+
} catch (error) {
|
|
10990
|
+
this.logDebug("battery_push_error", error);
|
|
10991
|
+
}
|
|
10992
|
+
}
|
|
10961
10993
|
if (frame.header.cmdId === 33) {
|
|
10962
10994
|
try {
|
|
10963
10995
|
const sid = this.socketSessionId;
|
|
@@ -18077,6 +18109,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18077
18109
|
}
|
|
18078
18110
|
}
|
|
18079
18111
|
const newClient = new BaichuanClient(this.clientOptions);
|
|
18112
|
+
this.attachD2cDiscListener(newClient);
|
|
18080
18113
|
this.socketPool.set("general", {
|
|
18081
18114
|
client: newClient,
|
|
18082
18115
|
refCount: 1,
|
|
@@ -18122,6 +18155,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18122
18155
|
if (!mapped) return;
|
|
18123
18156
|
this.dispatchSimpleEvent(mapped);
|
|
18124
18157
|
});
|
|
18158
|
+
client.on("batteryPush", (frame) => {
|
|
18159
|
+
try {
|
|
18160
|
+
const xml = this.client.tryDecryptXml(
|
|
18161
|
+
frame.body,
|
|
18162
|
+
frame.header.channelId,
|
|
18163
|
+
this.client.enc
|
|
18164
|
+
);
|
|
18165
|
+
if (!xml) return;
|
|
18166
|
+
const channel = frame.header.channelId;
|
|
18167
|
+
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
18168
|
+
if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
|
|
18169
|
+
this.dispatchSimpleEvent({
|
|
18170
|
+
type: "battery",
|
|
18171
|
+
channel,
|
|
18172
|
+
timestamp: Date.now(),
|
|
18173
|
+
battery
|
|
18174
|
+
});
|
|
18175
|
+
}
|
|
18176
|
+
} catch (e) {
|
|
18177
|
+
this.logger.debug?.(
|
|
18178
|
+
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
18179
|
+
formatErrorForLog(e)
|
|
18180
|
+
);
|
|
18181
|
+
}
|
|
18182
|
+
});
|
|
18125
18183
|
client.on("channelInfo", (xml) => {
|
|
18126
18184
|
try {
|
|
18127
18185
|
this.parseAndStoreChannelInfo(xml);
|
|
@@ -18257,6 +18315,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18257
18315
|
rtspServers = /* @__PURE__ */ new Set();
|
|
18258
18316
|
// Track all RTSP servers for cleanup
|
|
18259
18317
|
activeVideoMsgNums = /* @__PURE__ */ new Map();
|
|
18318
|
+
// ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
|
|
18319
|
+
// Tracked on the API instance (survives BaichuanClient recreation).
|
|
18320
|
+
/** Timestamp of the most recent D2C_DISC from any client for this device. */
|
|
18321
|
+
lastD2cDiscAtMs = 0;
|
|
18322
|
+
/** Sliding window of recent D2C_DISC timestamps for storm detection. */
|
|
18323
|
+
d2cDiscTimestamps = [];
|
|
18324
|
+
/** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
|
|
18325
|
+
* Prevents reconnect attempts while the camera is transitioning to sleep. */
|
|
18326
|
+
static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
|
|
18327
|
+
/** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
|
|
18328
|
+
static D2C_DISC_STORM_THRESHOLD = 3;
|
|
18329
|
+
/** Sliding window size (ms) for storm detection. */
|
|
18330
|
+
static D2C_DISC_STORM_WINDOW_MS = 6e4;
|
|
18331
|
+
/** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
|
|
18332
|
+
static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
|
|
18260
18333
|
nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
|
|
18261
18334
|
/**
|
|
18262
18335
|
* Cached device capabilities per channel.
|
|
@@ -18670,6 +18743,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18670
18743
|
}
|
|
18671
18744
|
return "general";
|
|
18672
18745
|
}
|
|
18746
|
+
/**
|
|
18747
|
+
* Attach a D2C_DISC listener to a BaichuanClient so that the API-level
|
|
18748
|
+
* grace period and storm detection are updated regardless of which
|
|
18749
|
+
* pool socket receives the disconnect.
|
|
18750
|
+
*/
|
|
18751
|
+
attachD2cDiscListener(client) {
|
|
18752
|
+
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
18753
|
+
}
|
|
18673
18754
|
/**
|
|
18674
18755
|
* Acquire a socket from the pool by tag.
|
|
18675
18756
|
* Creates a new socket if needed, or reuses an existing one.
|
|
@@ -18690,10 +18771,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18690
18771
|
);
|
|
18691
18772
|
} else if (now < cooldownEntry.cooldownUntil) {
|
|
18692
18773
|
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
18774
|
+
const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
|
|
18775
|
+
const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
|
|
18693
18776
|
const error = new Error(
|
|
18694
|
-
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to
|
|
18777
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
|
|
18695
18778
|
);
|
|
18696
|
-
log?.
|
|
18779
|
+
log?.debug?.(error.message);
|
|
18697
18780
|
throw error;
|
|
18698
18781
|
}
|
|
18699
18782
|
}
|
|
@@ -18784,12 +18867,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18784
18867
|
try {
|
|
18785
18868
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18786
18869
|
const newClient = new BaichuanClient(clientOpts);
|
|
18870
|
+
this.attachD2cDiscListener(newClient);
|
|
18787
18871
|
await newClient.login();
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
)
|
|
18792
|
-
|
|
18872
|
+
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18873
|
+
if (existingCooldown) {
|
|
18874
|
+
const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
|
|
18875
|
+
if (!isStormCooldown) {
|
|
18876
|
+
log?.debug?.(
|
|
18877
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
18878
|
+
);
|
|
18879
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
18880
|
+
} else {
|
|
18881
|
+
log?.debug?.(
|
|
18882
|
+
`[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
|
|
18883
|
+
);
|
|
18884
|
+
}
|
|
18793
18885
|
}
|
|
18794
18886
|
entry.client = newClient;
|
|
18795
18887
|
entry.refCount = 1;
|
|
@@ -19091,6 +19183,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19091
19183
|
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
19092
19184
|
};
|
|
19093
19185
|
const generalClient = new BaichuanClient(opts);
|
|
19186
|
+
this.attachD2cDiscListener(generalClient);
|
|
19094
19187
|
this.socketPool.set("general", {
|
|
19095
19188
|
client: generalClient,
|
|
19096
19189
|
refCount: 1,
|
|
@@ -24155,6 +24248,49 @@ ${stderr}`)
|
|
|
24155
24248
|
if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
|
|
24156
24249
|
return out;
|
|
24157
24250
|
}
|
|
24251
|
+
/**
|
|
24252
|
+
* Called when any BaichuanClient for this device receives a D2C_DISC.
|
|
24253
|
+
*
|
|
24254
|
+
* Two-tier response:
|
|
24255
|
+
* 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
|
|
24256
|
+
* (10 s) to prevent reconnect attempts while the camera transitions to sleep.
|
|
24257
|
+
* 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
|
|
24258
|
+
*/
|
|
24259
|
+
notifyD2cDisc() {
|
|
24260
|
+
const now = Date.now();
|
|
24261
|
+
this.lastD2cDiscAtMs = now;
|
|
24262
|
+
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
24263
|
+
const existing = this.socketPoolCooldowns.get(this.host);
|
|
24264
|
+
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
24265
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
24266
|
+
failureCount: existing?.failureCount ?? 1,
|
|
24267
|
+
lastFailureAt: now,
|
|
24268
|
+
cooldownUntil: immediateCooldownUntil
|
|
24269
|
+
});
|
|
24270
|
+
this.logger?.log?.(
|
|
24271
|
+
`[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
|
|
24272
|
+
);
|
|
24273
|
+
}
|
|
24274
|
+
this.d2cDiscTimestamps.push(now);
|
|
24275
|
+
const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
|
|
24276
|
+
while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
|
|
24277
|
+
this.d2cDiscTimestamps.shift();
|
|
24278
|
+
}
|
|
24279
|
+
if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
|
|
24280
|
+
const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
|
|
24281
|
+
const currentEntry = this.socketPoolCooldowns.get(this.host);
|
|
24282
|
+
if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
|
|
24283
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
24284
|
+
failureCount: this.d2cDiscTimestamps.length,
|
|
24285
|
+
lastFailureAt: now,
|
|
24286
|
+
cooldownUntil: stormCooldownUntil
|
|
24287
|
+
});
|
|
24288
|
+
this.logger?.warn?.(
|
|
24289
|
+
`[D2C_DISC] Storm detected: ${this.d2cDiscTimestamps.length} disconnects in ${_ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS / 1e3}s \u2192 socket pool cooldown ${_ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS / 1e3}s`
|
|
24290
|
+
);
|
|
24291
|
+
}
|
|
24292
|
+
}
|
|
24293
|
+
}
|
|
24158
24294
|
/**
|
|
24159
24295
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
24160
24296
|
*
|
|
@@ -24185,6 +24321,8 @@ ${stderr}`)
|
|
|
24185
24321
|
const socketConnected = this.client.isSocketConnected?.() ?? false;
|
|
24186
24322
|
const now = Date.now();
|
|
24187
24323
|
const cutoff = now - windowMs;
|
|
24324
|
+
const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
|
|
24325
|
+
const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
|
|
24188
24326
|
const rx = (this.client.getRxHistory?.() ?? []).filter(
|
|
24189
24327
|
(h) => h.atMs >= cutoff
|
|
24190
24328
|
);
|
|
@@ -24192,6 +24330,12 @@ ${stderr}`)
|
|
|
24192
24330
|
(h) => h.atMs >= cutoff
|
|
24193
24331
|
);
|
|
24194
24332
|
if (rx.length === 0 && tx.length === 0) {
|
|
24333
|
+
if (recentD2cDisc) {
|
|
24334
|
+
return {
|
|
24335
|
+
state: "sleeping",
|
|
24336
|
+
reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
24337
|
+
};
|
|
24338
|
+
}
|
|
24195
24339
|
return {
|
|
24196
24340
|
state: "sleeping",
|
|
24197
24341
|
reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
|
|
@@ -24215,6 +24359,12 @@ ${stderr}`)
|
|
|
24215
24359
|
idleMs: now - firstWakingTx.atMs
|
|
24216
24360
|
};
|
|
24217
24361
|
}
|
|
24362
|
+
if (recentD2cDisc) {
|
|
24363
|
+
return {
|
|
24364
|
+
state: "sleeping",
|
|
24365
|
+
reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
24366
|
+
};
|
|
24367
|
+
}
|
|
24218
24368
|
return {
|
|
24219
24369
|
state: "sleeping",
|
|
24220
24370
|
reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
|