@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/README.md
CHANGED
|
@@ -93,6 +93,8 @@ Devices with captured fixtures (verified API compatibility):
|
|
|
93
93
|
| --- | --- | --- |
|
|
94
94
|
| E1 Outdoor PoE | Wired camera | v3.1.0.5223 |
|
|
95
95
|
| E1 Zoom | Wired camera (H.265, PTZ) | v3.2.0.4741 |
|
|
96
|
+
| RLC-810A | Wired camera (8MP) | v3.1.0.1162 |
|
|
97
|
+
| B400 | Wired camera (4MP) | v3.0.0.183 |
|
|
96
98
|
| Argus 3E | Battery camera (via Home Hub) | v3.0.0.3623 |
|
|
97
99
|
| Argus PT Ultra | Battery camera with PTZ (via Home Hub) | v3.0.0.3911 |
|
|
98
100
|
| Reolink Home Hub | NVR / Hub | v3.3.0.456 |
|
|
@@ -1806,6 +1806,14 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1806
1806
|
* even if the current client instance is idle/disconnected.
|
|
1807
1807
|
*/
|
|
1808
1808
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
1809
|
+
/**
|
|
1810
|
+
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
1811
|
+
*
|
|
1812
|
+
* Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
|
|
1813
|
+
* and creates a new one. Instance-level backoff variables would reset to zero,
|
|
1814
|
+
* allowing immediate reconnection and perpetuating the storm.
|
|
1815
|
+
*/
|
|
1816
|
+
static d2cDiscBackoff = /* @__PURE__ */ new Map();
|
|
1809
1817
|
/**
|
|
1810
1818
|
* Global (process-wide) CoverPreview serialization.
|
|
1811
1819
|
*
|
|
@@ -2501,7 +2509,12 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2501
2509
|
}
|
|
2502
2510
|
async waitForUdpReconnectCooldown() {
|
|
2503
2511
|
const now = Date.now();
|
|
2504
|
-
const
|
|
2512
|
+
const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
|
|
2513
|
+
const effectiveCooldownUntil = Math.max(
|
|
2514
|
+
this.udpReconnectCooldownUntilMs,
|
|
2515
|
+
staticEntry?.cooldownUntilMs ?? 0
|
|
2516
|
+
);
|
|
2517
|
+
const waitMs = effectiveCooldownUntil - now;
|
|
2505
2518
|
if (waitMs <= 0) return;
|
|
2506
2519
|
const sid = this.socketSessionId;
|
|
2507
2520
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -2510,7 +2523,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2510
2523
|
host: this.opts.host,
|
|
2511
2524
|
sid,
|
|
2512
2525
|
uid: shortUid,
|
|
2513
|
-
waitMs
|
|
2526
|
+
waitMs,
|
|
2527
|
+
persistent: staticEntry != null
|
|
2514
2528
|
});
|
|
2515
2529
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
2516
2530
|
}
|
|
@@ -2753,21 +2767,30 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2753
2767
|
uid: shortUid2,
|
|
2754
2768
|
message: err.message
|
|
2755
2769
|
});
|
|
2756
|
-
const
|
|
2770
|
+
const hostKey = this.opts.host;
|
|
2771
|
+
const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
|
|
2772
|
+
const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
|
|
2757
2773
|
const baseMs = 2e3;
|
|
2758
2774
|
const maxMs = 3e4;
|
|
2759
2775
|
const nextBackoffMs = withinWindow ? Math.min(
|
|
2760
2776
|
maxMs,
|
|
2761
2777
|
Math.max(
|
|
2762
2778
|
baseMs,
|
|
2763
|
-
|
|
2779
|
+
prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
|
|
2764
2780
|
)
|
|
2765
2781
|
) : baseMs;
|
|
2766
|
-
|
|
2767
|
-
|
|
2782
|
+
const cooldownUntilMs = Math.max(
|
|
2783
|
+
prev?.cooldownUntilMs ?? 0,
|
|
2784
|
+
now + nextBackoffMs
|
|
2785
|
+
);
|
|
2786
|
+
_BaichuanClient.d2cDiscBackoff.set(hostKey, {
|
|
2787
|
+
backoffMs: nextBackoffMs,
|
|
2788
|
+
lastAtMs: now,
|
|
2789
|
+
cooldownUntilMs
|
|
2790
|
+
});
|
|
2768
2791
|
this.udpReconnectCooldownUntilMs = Math.max(
|
|
2769
2792
|
this.udpReconnectCooldownUntilMs,
|
|
2770
|
-
|
|
2793
|
+
cooldownUntilMs
|
|
2771
2794
|
);
|
|
2772
2795
|
this.logDebug("d2c_disc_backoff", {
|
|
2773
2796
|
transport: "udp",
|
|
@@ -2775,7 +2798,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2775
2798
|
sid: sid2,
|
|
2776
2799
|
uid: shortUid2,
|
|
2777
2800
|
backoffMs: nextBackoffMs,
|
|
2778
|
-
cooldownUntilMs
|
|
2801
|
+
cooldownUntilMs,
|
|
2802
|
+
persistent: true
|
|
2779
2803
|
});
|
|
2780
2804
|
this.stopKeepAlive();
|
|
2781
2805
|
this.loggedIn = false;
|
|
@@ -2784,6 +2808,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2784
2808
|
this.videoSubscriptions.clear();
|
|
2785
2809
|
this.recomputeGlobalStreamingContribution();
|
|
2786
2810
|
}
|
|
2811
|
+
this.emit("d2c_disc", { host: this.opts.host, atMs: now });
|
|
2787
2812
|
}
|
|
2788
2813
|
this.emit("error", err);
|
|
2789
2814
|
});
|
|
@@ -3082,6 +3107,13 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
3082
3107
|
}
|
|
3083
3108
|
}
|
|
3084
3109
|
this.emit("push", frame);
|
|
3110
|
+
if (frame.header.cmdId === 252 && frame.body.length > 0) {
|
|
3111
|
+
try {
|
|
3112
|
+
this.emit("batteryPush", frame);
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
this.logDebug("battery_push_error", error);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3085
3117
|
if (frame.header.cmdId === 33) {
|
|
3086
3118
|
try {
|
|
3087
3119
|
const sid = this.socketSessionId;
|
|
@@ -10149,6 +10181,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10149
10181
|
}
|
|
10150
10182
|
}
|
|
10151
10183
|
const newClient = new BaichuanClient(this.clientOptions);
|
|
10184
|
+
this.attachD2cDiscListener(newClient);
|
|
10152
10185
|
this.socketPool.set("general", {
|
|
10153
10186
|
client: newClient,
|
|
10154
10187
|
refCount: 1,
|
|
@@ -10194,6 +10227,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10194
10227
|
if (!mapped) return;
|
|
10195
10228
|
this.dispatchSimpleEvent(mapped);
|
|
10196
10229
|
});
|
|
10230
|
+
client.on("batteryPush", (frame) => {
|
|
10231
|
+
try {
|
|
10232
|
+
const xml = this.client.tryDecryptXml(
|
|
10233
|
+
frame.body,
|
|
10234
|
+
frame.header.channelId,
|
|
10235
|
+
this.client.enc
|
|
10236
|
+
);
|
|
10237
|
+
if (!xml) return;
|
|
10238
|
+
const channel = frame.header.channelId;
|
|
10239
|
+
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
10240
|
+
if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
|
|
10241
|
+
this.dispatchSimpleEvent({
|
|
10242
|
+
type: "battery",
|
|
10243
|
+
channel,
|
|
10244
|
+
timestamp: Date.now(),
|
|
10245
|
+
battery
|
|
10246
|
+
});
|
|
10247
|
+
}
|
|
10248
|
+
} catch (e) {
|
|
10249
|
+
this.logger.debug?.(
|
|
10250
|
+
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
10251
|
+
formatErrorForLog(e)
|
|
10252
|
+
);
|
|
10253
|
+
}
|
|
10254
|
+
});
|
|
10197
10255
|
client.on("channelInfo", (xml) => {
|
|
10198
10256
|
try {
|
|
10199
10257
|
this.parseAndStoreChannelInfo(xml);
|
|
@@ -10329,6 +10387,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10329
10387
|
rtspServers = /* @__PURE__ */ new Set();
|
|
10330
10388
|
// Track all RTSP servers for cleanup
|
|
10331
10389
|
activeVideoMsgNums = /* @__PURE__ */ new Map();
|
|
10390
|
+
// ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
|
|
10391
|
+
// Tracked on the API instance (survives BaichuanClient recreation).
|
|
10392
|
+
/** Timestamp of the most recent D2C_DISC from any client for this device. */
|
|
10393
|
+
lastD2cDiscAtMs = 0;
|
|
10394
|
+
/** Sliding window of recent D2C_DISC timestamps for storm detection. */
|
|
10395
|
+
d2cDiscTimestamps = [];
|
|
10396
|
+
/** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
|
|
10397
|
+
* Prevents reconnect attempts while the camera is transitioning to sleep. */
|
|
10398
|
+
static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
|
|
10399
|
+
/** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
|
|
10400
|
+
static D2C_DISC_STORM_THRESHOLD = 3;
|
|
10401
|
+
/** Sliding window size (ms) for storm detection. */
|
|
10402
|
+
static D2C_DISC_STORM_WINDOW_MS = 6e4;
|
|
10403
|
+
/** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
|
|
10404
|
+
static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
|
|
10332
10405
|
nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
|
|
10333
10406
|
/**
|
|
10334
10407
|
* Cached device capabilities per channel.
|
|
@@ -10742,6 +10815,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10742
10815
|
}
|
|
10743
10816
|
return "general";
|
|
10744
10817
|
}
|
|
10818
|
+
/**
|
|
10819
|
+
* Attach a D2C_DISC listener to a BaichuanClient so that the API-level
|
|
10820
|
+
* grace period and storm detection are updated regardless of which
|
|
10821
|
+
* pool socket receives the disconnect.
|
|
10822
|
+
*/
|
|
10823
|
+
attachD2cDiscListener(client) {
|
|
10824
|
+
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
10825
|
+
}
|
|
10745
10826
|
/**
|
|
10746
10827
|
* Acquire a socket from the pool by tag.
|
|
10747
10828
|
* Creates a new socket if needed, or reuses an existing one.
|
|
@@ -10762,10 +10843,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10762
10843
|
);
|
|
10763
10844
|
} else if (now < cooldownEntry.cooldownUntil) {
|
|
10764
10845
|
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
10846
|
+
const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
|
|
10847
|
+
const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
|
|
10765
10848
|
const error = new Error(
|
|
10766
|
-
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to
|
|
10849
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
|
|
10767
10850
|
);
|
|
10768
|
-
log?.
|
|
10851
|
+
log?.debug?.(error.message);
|
|
10769
10852
|
throw error;
|
|
10770
10853
|
}
|
|
10771
10854
|
}
|
|
@@ -10856,12 +10939,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10856
10939
|
try {
|
|
10857
10940
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
10858
10941
|
const newClient = new BaichuanClient(clientOpts);
|
|
10942
|
+
this.attachD2cDiscListener(newClient);
|
|
10859
10943
|
await newClient.login();
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
)
|
|
10864
|
-
|
|
10944
|
+
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
10945
|
+
if (existingCooldown) {
|
|
10946
|
+
const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
|
|
10947
|
+
if (!isStormCooldown) {
|
|
10948
|
+
log?.debug?.(
|
|
10949
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
10950
|
+
);
|
|
10951
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
10952
|
+
} else {
|
|
10953
|
+
log?.debug?.(
|
|
10954
|
+
`[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
|
|
10955
|
+
);
|
|
10956
|
+
}
|
|
10865
10957
|
}
|
|
10866
10958
|
entry.client = newClient;
|
|
10867
10959
|
entry.refCount = 1;
|
|
@@ -11163,6 +11255,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11163
11255
|
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
11164
11256
|
};
|
|
11165
11257
|
const generalClient = new BaichuanClient(opts);
|
|
11258
|
+
this.attachD2cDiscListener(generalClient);
|
|
11166
11259
|
this.socketPool.set("general", {
|
|
11167
11260
|
client: generalClient,
|
|
11168
11261
|
refCount: 1,
|
|
@@ -16227,6 +16320,49 @@ ${stderr}`)
|
|
|
16227
16320
|
if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
|
|
16228
16321
|
return out;
|
|
16229
16322
|
}
|
|
16323
|
+
/**
|
|
16324
|
+
* Called when any BaichuanClient for this device receives a D2C_DISC.
|
|
16325
|
+
*
|
|
16326
|
+
* Two-tier response:
|
|
16327
|
+
* 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
|
|
16328
|
+
* (10 s) to prevent reconnect attempts while the camera transitions to sleep.
|
|
16329
|
+
* 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
|
|
16330
|
+
*/
|
|
16331
|
+
notifyD2cDisc() {
|
|
16332
|
+
const now = Date.now();
|
|
16333
|
+
this.lastD2cDiscAtMs = now;
|
|
16334
|
+
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
16335
|
+
const existing = this.socketPoolCooldowns.get(this.host);
|
|
16336
|
+
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
16337
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
16338
|
+
failureCount: existing?.failureCount ?? 1,
|
|
16339
|
+
lastFailureAt: now,
|
|
16340
|
+
cooldownUntil: immediateCooldownUntil
|
|
16341
|
+
});
|
|
16342
|
+
this.logger?.log?.(
|
|
16343
|
+
`[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
|
|
16344
|
+
);
|
|
16345
|
+
}
|
|
16346
|
+
this.d2cDiscTimestamps.push(now);
|
|
16347
|
+
const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
|
|
16348
|
+
while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
|
|
16349
|
+
this.d2cDiscTimestamps.shift();
|
|
16350
|
+
}
|
|
16351
|
+
if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
|
|
16352
|
+
const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
|
|
16353
|
+
const currentEntry = this.socketPoolCooldowns.get(this.host);
|
|
16354
|
+
if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
|
|
16355
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
16356
|
+
failureCount: this.d2cDiscTimestamps.length,
|
|
16357
|
+
lastFailureAt: now,
|
|
16358
|
+
cooldownUntil: stormCooldownUntil
|
|
16359
|
+
});
|
|
16360
|
+
this.logger?.warn?.(
|
|
16361
|
+
`[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`
|
|
16362
|
+
);
|
|
16363
|
+
}
|
|
16364
|
+
}
|
|
16365
|
+
}
|
|
16230
16366
|
/**
|
|
16231
16367
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
16232
16368
|
*
|
|
@@ -16257,6 +16393,8 @@ ${stderr}`)
|
|
|
16257
16393
|
const socketConnected = this.client.isSocketConnected?.() ?? false;
|
|
16258
16394
|
const now = Date.now();
|
|
16259
16395
|
const cutoff = now - windowMs;
|
|
16396
|
+
const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
|
|
16397
|
+
const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
|
|
16260
16398
|
const rx = (this.client.getRxHistory?.() ?? []).filter(
|
|
16261
16399
|
(h) => h.atMs >= cutoff
|
|
16262
16400
|
);
|
|
@@ -16264,6 +16402,12 @@ ${stderr}`)
|
|
|
16264
16402
|
(h) => h.atMs >= cutoff
|
|
16265
16403
|
);
|
|
16266
16404
|
if (rx.length === 0 && tx.length === 0) {
|
|
16405
|
+
if (recentD2cDisc) {
|
|
16406
|
+
return {
|
|
16407
|
+
state: "sleeping",
|
|
16408
|
+
reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
16409
|
+
};
|
|
16410
|
+
}
|
|
16267
16411
|
return {
|
|
16268
16412
|
state: "sleeping",
|
|
16269
16413
|
reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
|
|
@@ -16287,6 +16431,12 @@ ${stderr}`)
|
|
|
16287
16431
|
idleMs: now - firstWakingTx.atMs
|
|
16288
16432
|
};
|
|
16289
16433
|
}
|
|
16434
|
+
if (recentD2cDisc) {
|
|
16435
|
+
return {
|
|
16436
|
+
state: "sleeping",
|
|
16437
|
+
reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
16438
|
+
};
|
|
16439
|
+
}
|
|
16290
16440
|
return {
|
|
16291
16441
|
state: "sleeping",
|
|
16292
16442
|
reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
|
|
@@ -21580,4 +21730,4 @@ export {
|
|
|
21580
21730
|
isTcpFailureThatShouldFallbackToUdp,
|
|
21581
21731
|
autoDetectDeviceType
|
|
21582
21732
|
};
|
|
21583
|
-
//# sourceMappingURL=chunk-
|
|
21733
|
+
//# sourceMappingURL=chunk-WDFKIHM5.js.map
|