@apocaliss92/nodelink-js 0.4.3 → 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.cjs
CHANGED
|
@@ -11851,6 +11851,14 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
11851
11851
|
* even if the current client instance is idle/disconnected.
|
|
11852
11852
|
*/
|
|
11853
11853
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
11854
|
+
/**
|
|
11855
|
+
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
11856
|
+
*
|
|
11857
|
+
* Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
|
|
11858
|
+
* and creates a new one. Instance-level backoff variables would reset to zero,
|
|
11859
|
+
* allowing immediate reconnection and perpetuating the storm.
|
|
11860
|
+
*/
|
|
11861
|
+
static d2cDiscBackoff = /* @__PURE__ */ new Map();
|
|
11854
11862
|
/**
|
|
11855
11863
|
* Global (process-wide) CoverPreview serialization.
|
|
11856
11864
|
*
|
|
@@ -12546,7 +12554,12 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12546
12554
|
}
|
|
12547
12555
|
async waitForUdpReconnectCooldown() {
|
|
12548
12556
|
const now = Date.now();
|
|
12549
|
-
const
|
|
12557
|
+
const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
|
|
12558
|
+
const effectiveCooldownUntil = Math.max(
|
|
12559
|
+
this.udpReconnectCooldownUntilMs,
|
|
12560
|
+
staticEntry?.cooldownUntilMs ?? 0
|
|
12561
|
+
);
|
|
12562
|
+
const waitMs = effectiveCooldownUntil - now;
|
|
12550
12563
|
if (waitMs <= 0) return;
|
|
12551
12564
|
const sid = this.socketSessionId;
|
|
12552
12565
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -12555,7 +12568,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12555
12568
|
host: this.opts.host,
|
|
12556
12569
|
sid,
|
|
12557
12570
|
uid: shortUid,
|
|
12558
|
-
waitMs
|
|
12571
|
+
waitMs,
|
|
12572
|
+
persistent: staticEntry != null
|
|
12559
12573
|
});
|
|
12560
12574
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
12561
12575
|
}
|
|
@@ -12798,21 +12812,30 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12798
12812
|
uid: shortUid2,
|
|
12799
12813
|
message: err.message
|
|
12800
12814
|
});
|
|
12801
|
-
const
|
|
12815
|
+
const hostKey = this.opts.host;
|
|
12816
|
+
const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
|
|
12817
|
+
const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
|
|
12802
12818
|
const baseMs = 2e3;
|
|
12803
12819
|
const maxMs = 3e4;
|
|
12804
12820
|
const nextBackoffMs = withinWindow ? Math.min(
|
|
12805
12821
|
maxMs,
|
|
12806
12822
|
Math.max(
|
|
12807
12823
|
baseMs,
|
|
12808
|
-
|
|
12824
|
+
prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
|
|
12809
12825
|
)
|
|
12810
12826
|
) : baseMs;
|
|
12811
|
-
|
|
12812
|
-
|
|
12827
|
+
const cooldownUntilMs = Math.max(
|
|
12828
|
+
prev?.cooldownUntilMs ?? 0,
|
|
12829
|
+
now + nextBackoffMs
|
|
12830
|
+
);
|
|
12831
|
+
_BaichuanClient.d2cDiscBackoff.set(hostKey, {
|
|
12832
|
+
backoffMs: nextBackoffMs,
|
|
12833
|
+
lastAtMs: now,
|
|
12834
|
+
cooldownUntilMs
|
|
12835
|
+
});
|
|
12813
12836
|
this.udpReconnectCooldownUntilMs = Math.max(
|
|
12814
12837
|
this.udpReconnectCooldownUntilMs,
|
|
12815
|
-
|
|
12838
|
+
cooldownUntilMs
|
|
12816
12839
|
);
|
|
12817
12840
|
this.logDebug("d2c_disc_backoff", {
|
|
12818
12841
|
transport: "udp",
|
|
@@ -12820,7 +12843,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12820
12843
|
sid: sid2,
|
|
12821
12844
|
uid: shortUid2,
|
|
12822
12845
|
backoffMs: nextBackoffMs,
|
|
12823
|
-
cooldownUntilMs
|
|
12846
|
+
cooldownUntilMs,
|
|
12847
|
+
persistent: true
|
|
12824
12848
|
});
|
|
12825
12849
|
this.stopKeepAlive();
|
|
12826
12850
|
this.loggedIn = false;
|
|
@@ -12829,6 +12853,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12829
12853
|
this.videoSubscriptions.clear();
|
|
12830
12854
|
this.recomputeGlobalStreamingContribution();
|
|
12831
12855
|
}
|
|
12856
|
+
this.emit("d2c_disc", { host: this.opts.host, atMs: now });
|
|
12832
12857
|
}
|
|
12833
12858
|
this.emit("error", err);
|
|
12834
12859
|
});
|
|
@@ -13127,6 +13152,13 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
13127
13152
|
}
|
|
13128
13153
|
}
|
|
13129
13154
|
this.emit("push", frame);
|
|
13155
|
+
if (frame.header.cmdId === 252 && frame.body.length > 0) {
|
|
13156
|
+
try {
|
|
13157
|
+
this.emit("batteryPush", frame);
|
|
13158
|
+
} catch (error) {
|
|
13159
|
+
this.logDebug("battery_push_error", error);
|
|
13160
|
+
}
|
|
13161
|
+
}
|
|
13130
13162
|
if (frame.header.cmdId === 33) {
|
|
13131
13163
|
try {
|
|
13132
13164
|
const sid = this.socketSessionId;
|
|
@@ -17500,6 +17532,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17500
17532
|
}
|
|
17501
17533
|
}
|
|
17502
17534
|
const newClient = new BaichuanClient(this.clientOptions);
|
|
17535
|
+
this.attachD2cDiscListener(newClient);
|
|
17503
17536
|
this.socketPool.set("general", {
|
|
17504
17537
|
client: newClient,
|
|
17505
17538
|
refCount: 1,
|
|
@@ -17545,6 +17578,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17545
17578
|
if (!mapped) return;
|
|
17546
17579
|
this.dispatchSimpleEvent(mapped);
|
|
17547
17580
|
});
|
|
17581
|
+
client.on("batteryPush", (frame) => {
|
|
17582
|
+
try {
|
|
17583
|
+
const xml = this.client.tryDecryptXml(
|
|
17584
|
+
frame.body,
|
|
17585
|
+
frame.header.channelId,
|
|
17586
|
+
this.client.enc
|
|
17587
|
+
);
|
|
17588
|
+
if (!xml) return;
|
|
17589
|
+
const channel = frame.header.channelId;
|
|
17590
|
+
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
17591
|
+
if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
|
|
17592
|
+
this.dispatchSimpleEvent({
|
|
17593
|
+
type: "battery",
|
|
17594
|
+
channel,
|
|
17595
|
+
timestamp: Date.now(),
|
|
17596
|
+
battery
|
|
17597
|
+
});
|
|
17598
|
+
}
|
|
17599
|
+
} catch (e) {
|
|
17600
|
+
this.logger.debug?.(
|
|
17601
|
+
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
17602
|
+
formatErrorForLog(e)
|
|
17603
|
+
);
|
|
17604
|
+
}
|
|
17605
|
+
});
|
|
17548
17606
|
client.on("channelInfo", (xml) => {
|
|
17549
17607
|
try {
|
|
17550
17608
|
this.parseAndStoreChannelInfo(xml);
|
|
@@ -17680,6 +17738,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17680
17738
|
rtspServers = /* @__PURE__ */ new Set();
|
|
17681
17739
|
// Track all RTSP servers for cleanup
|
|
17682
17740
|
activeVideoMsgNums = /* @__PURE__ */ new Map();
|
|
17741
|
+
// ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
|
|
17742
|
+
// Tracked on the API instance (survives BaichuanClient recreation).
|
|
17743
|
+
/** Timestamp of the most recent D2C_DISC from any client for this device. */
|
|
17744
|
+
lastD2cDiscAtMs = 0;
|
|
17745
|
+
/** Sliding window of recent D2C_DISC timestamps for storm detection. */
|
|
17746
|
+
d2cDiscTimestamps = [];
|
|
17747
|
+
/** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
|
|
17748
|
+
* Prevents reconnect attempts while the camera is transitioning to sleep. */
|
|
17749
|
+
static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
|
|
17750
|
+
/** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
|
|
17751
|
+
static D2C_DISC_STORM_THRESHOLD = 3;
|
|
17752
|
+
/** Sliding window size (ms) for storm detection. */
|
|
17753
|
+
static D2C_DISC_STORM_WINDOW_MS = 6e4;
|
|
17754
|
+
/** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
|
|
17755
|
+
static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
|
|
17683
17756
|
nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
|
|
17684
17757
|
/**
|
|
17685
17758
|
* Cached device capabilities per channel.
|
|
@@ -18093,6 +18166,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18093
18166
|
}
|
|
18094
18167
|
return "general";
|
|
18095
18168
|
}
|
|
18169
|
+
/**
|
|
18170
|
+
* Attach a D2C_DISC listener to a BaichuanClient so that the API-level
|
|
18171
|
+
* grace period and storm detection are updated regardless of which
|
|
18172
|
+
* pool socket receives the disconnect.
|
|
18173
|
+
*/
|
|
18174
|
+
attachD2cDiscListener(client) {
|
|
18175
|
+
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
18176
|
+
}
|
|
18096
18177
|
/**
|
|
18097
18178
|
* Acquire a socket from the pool by tag.
|
|
18098
18179
|
* Creates a new socket if needed, or reuses an existing one.
|
|
@@ -18113,10 +18194,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18113
18194
|
);
|
|
18114
18195
|
} else if (now < cooldownEntry.cooldownUntil) {
|
|
18115
18196
|
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
18197
|
+
const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
|
|
18198
|
+
const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
|
|
18116
18199
|
const error = new Error(
|
|
18117
|
-
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to
|
|
18200
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
|
|
18118
18201
|
);
|
|
18119
|
-
log?.
|
|
18202
|
+
log?.debug?.(error.message);
|
|
18120
18203
|
throw error;
|
|
18121
18204
|
}
|
|
18122
18205
|
}
|
|
@@ -18207,12 +18290,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18207
18290
|
try {
|
|
18208
18291
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18209
18292
|
const newClient = new BaichuanClient(clientOpts);
|
|
18293
|
+
this.attachD2cDiscListener(newClient);
|
|
18210
18294
|
await newClient.login();
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
)
|
|
18215
|
-
|
|
18295
|
+
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18296
|
+
if (existingCooldown) {
|
|
18297
|
+
const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
|
|
18298
|
+
if (!isStormCooldown) {
|
|
18299
|
+
log?.debug?.(
|
|
18300
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
18301
|
+
);
|
|
18302
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
18303
|
+
} else {
|
|
18304
|
+
log?.debug?.(
|
|
18305
|
+
`[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
|
|
18306
|
+
);
|
|
18307
|
+
}
|
|
18216
18308
|
}
|
|
18217
18309
|
entry.client = newClient;
|
|
18218
18310
|
entry.refCount = 1;
|
|
@@ -18514,6 +18606,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18514
18606
|
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
18515
18607
|
};
|
|
18516
18608
|
const generalClient = new BaichuanClient(opts);
|
|
18609
|
+
this.attachD2cDiscListener(generalClient);
|
|
18517
18610
|
this.socketPool.set("general", {
|
|
18518
18611
|
client: generalClient,
|
|
18519
18612
|
refCount: 1,
|
|
@@ -23578,6 +23671,49 @@ ${stderr}`)
|
|
|
23578
23671
|
if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
|
|
23579
23672
|
return out;
|
|
23580
23673
|
}
|
|
23674
|
+
/**
|
|
23675
|
+
* Called when any BaichuanClient for this device receives a D2C_DISC.
|
|
23676
|
+
*
|
|
23677
|
+
* Two-tier response:
|
|
23678
|
+
* 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
|
|
23679
|
+
* (10 s) to prevent reconnect attempts while the camera transitions to sleep.
|
|
23680
|
+
* 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
|
|
23681
|
+
*/
|
|
23682
|
+
notifyD2cDisc() {
|
|
23683
|
+
const now = Date.now();
|
|
23684
|
+
this.lastD2cDiscAtMs = now;
|
|
23685
|
+
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
23686
|
+
const existing = this.socketPoolCooldowns.get(this.host);
|
|
23687
|
+
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
23688
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
23689
|
+
failureCount: existing?.failureCount ?? 1,
|
|
23690
|
+
lastFailureAt: now,
|
|
23691
|
+
cooldownUntil: immediateCooldownUntil
|
|
23692
|
+
});
|
|
23693
|
+
this.logger?.log?.(
|
|
23694
|
+
`[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
|
|
23695
|
+
);
|
|
23696
|
+
}
|
|
23697
|
+
this.d2cDiscTimestamps.push(now);
|
|
23698
|
+
const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
|
|
23699
|
+
while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
|
|
23700
|
+
this.d2cDiscTimestamps.shift();
|
|
23701
|
+
}
|
|
23702
|
+
if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
|
|
23703
|
+
const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
|
|
23704
|
+
const currentEntry = this.socketPoolCooldowns.get(this.host);
|
|
23705
|
+
if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
|
|
23706
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
23707
|
+
failureCount: this.d2cDiscTimestamps.length,
|
|
23708
|
+
lastFailureAt: now,
|
|
23709
|
+
cooldownUntil: stormCooldownUntil
|
|
23710
|
+
});
|
|
23711
|
+
this.logger?.warn?.(
|
|
23712
|
+
`[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`
|
|
23713
|
+
);
|
|
23714
|
+
}
|
|
23715
|
+
}
|
|
23716
|
+
}
|
|
23581
23717
|
/**
|
|
23582
23718
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
23583
23719
|
*
|
|
@@ -23608,6 +23744,8 @@ ${stderr}`)
|
|
|
23608
23744
|
const socketConnected = this.client.isSocketConnected?.() ?? false;
|
|
23609
23745
|
const now = Date.now();
|
|
23610
23746
|
const cutoff = now - windowMs;
|
|
23747
|
+
const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
|
|
23748
|
+
const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
|
|
23611
23749
|
const rx = (this.client.getRxHistory?.() ?? []).filter(
|
|
23612
23750
|
(h) => h.atMs >= cutoff
|
|
23613
23751
|
);
|
|
@@ -23615,6 +23753,12 @@ ${stderr}`)
|
|
|
23615
23753
|
(h) => h.atMs >= cutoff
|
|
23616
23754
|
);
|
|
23617
23755
|
if (rx.length === 0 && tx.length === 0) {
|
|
23756
|
+
if (recentD2cDisc) {
|
|
23757
|
+
return {
|
|
23758
|
+
state: "sleeping",
|
|
23759
|
+
reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
23760
|
+
};
|
|
23761
|
+
}
|
|
23618
23762
|
return {
|
|
23619
23763
|
state: "sleeping",
|
|
23620
23764
|
reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
|
|
@@ -23638,6 +23782,12 @@ ${stderr}`)
|
|
|
23638
23782
|
idleMs: now - firstWakingTx.atMs
|
|
23639
23783
|
};
|
|
23640
23784
|
}
|
|
23785
|
+
if (recentD2cDisc) {
|
|
23786
|
+
return {
|
|
23787
|
+
state: "sleeping",
|
|
23788
|
+
reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
23789
|
+
};
|
|
23790
|
+
}
|
|
23641
23791
|
return {
|
|
23642
23792
|
state: "sleeping",
|
|
23643
23793
|
reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
|