@apocaliss92/nodelink-js 0.4.4 → 0.4.6
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/{DiagnosticsTools-55PR4WFD.js → DiagnosticsTools-UMN4C7SY.js} +2 -2
- package/dist/{chunk-UHFJPQA4.js → chunk-F2Y5U3YP.js} +251 -28
- package/dist/chunk-F2Y5U3YP.js.map +1 -0
- package/dist/{chunk-DEOMUWBN.js → chunk-TR3V5FTO.js} +15 -1
- package/dist/chunk-TR3V5FTO.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +261 -24
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +290 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -1
- package/dist/index.d.ts +66 -1
- package/dist/index.js +31 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-DEOMUWBN.js.map +0 -1
- package/dist/chunk-UHFJPQA4.js.map +0 -1
- /package/dist/{DiagnosticsTools-55PR4WFD.js.map → DiagnosticsTools-UMN4C7SY.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -1567,6 +1567,19 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1567
1567
|
// Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
|
|
1568
1568
|
// In CFB mode, continuation frames must use the cipher state from previous frames.
|
|
1569
1569
|
aesStreamDecryptor = null;
|
|
1570
|
+
/**
|
|
1571
|
+
* Pending startup error stashed when emitSafeError is called before any
|
|
1572
|
+
* "error" listener is registered (e.g. camera returns 400 during start()).
|
|
1573
|
+
* The rfc4571-server's waitForKeyframe can consume this immediately instead
|
|
1574
|
+
* of waiting for the full keyframe timeout.
|
|
1575
|
+
*/
|
|
1576
|
+
_pendingStartupError;
|
|
1577
|
+
/** Consume and clear any pending startup error. */
|
|
1578
|
+
consumePendingStartupError() {
|
|
1579
|
+
const err = this._pendingStartupError;
|
|
1580
|
+
this._pendingStartupError = void 0;
|
|
1581
|
+
return err;
|
|
1582
|
+
}
|
|
1570
1583
|
emitSafeError(err) {
|
|
1571
1584
|
if (!this.active) {
|
|
1572
1585
|
this.logger?.warn?.(
|
|
@@ -1578,6 +1591,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1578
1591
|
this.logger?.warn?.(
|
|
1579
1592
|
`[BaichuanVideoStream] Unhandled stream error: ${err.message}`
|
|
1580
1593
|
);
|
|
1594
|
+
this._pendingStartupError = err;
|
|
1581
1595
|
return;
|
|
1582
1596
|
}
|
|
1583
1597
|
this.emit("error", err);
|
|
@@ -10657,6 +10671,9 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
10657
10671
|
resendTimer;
|
|
10658
10672
|
hbTimer;
|
|
10659
10673
|
discoveryTid;
|
|
10674
|
+
// Track discovery-phase timers so close() can cancel them even if
|
|
10675
|
+
// discovery is still in progress (prevents ERR_SOCKET_DGRAM_NOT_RUNNING).
|
|
10676
|
+
discoveryTimers = [];
|
|
10660
10677
|
acceptSent = false;
|
|
10661
10678
|
lastAcceptAtMs;
|
|
10662
10679
|
ackScheduled = false;
|
|
@@ -10705,9 +10722,31 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
10705
10722
|
});
|
|
10706
10723
|
sock.on("error", (e) => this.emit("error", e));
|
|
10707
10724
|
sock.on("close", () => this.emit("close"));
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
10725
|
+
const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
|
|
10726
|
+
for (let i = portRange.length - 1; i > 0; i--) {
|
|
10727
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
10728
|
+
[portRange[i], portRange[j]] = [portRange[j], portRange[i]];
|
|
10729
|
+
}
|
|
10730
|
+
let bound = false;
|
|
10731
|
+
for (const port of portRange) {
|
|
10732
|
+
try {
|
|
10733
|
+
await new Promise((resolve, reject) => {
|
|
10734
|
+
sock.once("error", reject);
|
|
10735
|
+
sock.bind(port, "0.0.0.0", () => {
|
|
10736
|
+
sock.removeListener("error", reject);
|
|
10737
|
+
resolve();
|
|
10738
|
+
});
|
|
10739
|
+
});
|
|
10740
|
+
bound = true;
|
|
10741
|
+
break;
|
|
10742
|
+
} catch {
|
|
10743
|
+
}
|
|
10744
|
+
}
|
|
10745
|
+
if (!bound) {
|
|
10746
|
+
await new Promise(
|
|
10747
|
+
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
10748
|
+
);
|
|
10749
|
+
}
|
|
10711
10750
|
if (this.opts.mode === "direct") {
|
|
10712
10751
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
10713
10752
|
this.clientId = this.opts.clientId;
|
|
@@ -11127,7 +11166,24 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11127
11166
|
BCUDP_DISCOVERY_PORT_LOCAL_ANY,
|
|
11128
11167
|
BCUDP_DISCOVERY_PORT_LOCAL_UID
|
|
11129
11168
|
];
|
|
11130
|
-
const
|
|
11169
|
+
const broadcastHosts = ["255.255.255.255"];
|
|
11170
|
+
const ifaces = (0, import_node_os.networkInterfaces)();
|
|
11171
|
+
for (const name of Object.keys(ifaces)) {
|
|
11172
|
+
const entries = ifaces[name];
|
|
11173
|
+
if (!entries) continue;
|
|
11174
|
+
for (const addr2 of entries) {
|
|
11175
|
+
if (addr2.family === "IPv4" && !addr2.internal && addr2.cidr) {
|
|
11176
|
+
const ipParts = addr2.address.split(".").map(Number);
|
|
11177
|
+
const maskParts = addr2.netmask.split(".").map(Number);
|
|
11178
|
+
if (ipParts.length === 4 && maskParts.length === 4) {
|
|
11179
|
+
const bcast = ipParts.map((octet, i) => octet | ~maskParts[i] & 255).join(".");
|
|
11180
|
+
if (!broadcastHosts.includes(bcast)) {
|
|
11181
|
+
broadcastHosts.push(bcast);
|
|
11182
|
+
}
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
}
|
|
11131
11187
|
const directHost = (this.opts.directHost ?? "").trim();
|
|
11132
11188
|
const localMode = opts?.localMode ?? "local-broadcast";
|
|
11133
11189
|
const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
|
|
@@ -11154,6 +11210,7 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11154
11210
|
)
|
|
11155
11211
|
);
|
|
11156
11212
|
}, discoveryTimeout);
|
|
11213
|
+
this.discoveryTimers.push(timeout);
|
|
11157
11214
|
let retryTimer;
|
|
11158
11215
|
let retryCount = 0;
|
|
11159
11216
|
let discoveredSid;
|
|
@@ -11330,11 +11387,11 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11330
11387
|
if (directHost) {
|
|
11331
11388
|
if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
|
|
11332
11389
|
return [directHost];
|
|
11333
|
-
return [directHost,
|
|
11390
|
+
return [directHost, ...broadcastHosts];
|
|
11334
11391
|
}
|
|
11335
|
-
return
|
|
11392
|
+
return broadcastHosts;
|
|
11336
11393
|
}
|
|
11337
|
-
return
|
|
11394
|
+
return broadcastHosts;
|
|
11338
11395
|
})();
|
|
11339
11396
|
for (const host of Array.from(new Set(hosts))) {
|
|
11340
11397
|
for (const port of ports) {
|
|
@@ -11342,8 +11399,7 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11342
11399
|
sock.send(packet, port, host);
|
|
11343
11400
|
retryCount++;
|
|
11344
11401
|
this.emit("debug", "discovery_send", { retryCount, host, port });
|
|
11345
|
-
} catch
|
|
11346
|
-
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
11402
|
+
} catch {
|
|
11347
11403
|
}
|
|
11348
11404
|
}
|
|
11349
11405
|
}
|
|
@@ -11352,6 +11408,7 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11352
11408
|
retryTimer = (0, import_node_timers.setInterval)(() => {
|
|
11353
11409
|
sendDiscovery();
|
|
11354
11410
|
}, retryInterval);
|
|
11411
|
+
this.discoveryTimers.push(retryTimer);
|
|
11355
11412
|
});
|
|
11356
11413
|
this.clientId = reply.cid;
|
|
11357
11414
|
this.cameraId = reply.did;
|
|
@@ -11668,6 +11725,10 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
|
|
|
11668
11725
|
this.ackTimer = void 0;
|
|
11669
11726
|
this.resendTimer = void 0;
|
|
11670
11727
|
this.hbTimer = void 0;
|
|
11728
|
+
for (const t of this.discoveryTimers) {
|
|
11729
|
+
clearInterval(t);
|
|
11730
|
+
}
|
|
11731
|
+
this.discoveryTimers = [];
|
|
11671
11732
|
const s = this.sock;
|
|
11672
11733
|
this.sock = void 0;
|
|
11673
11734
|
if (!s) return;
|
|
@@ -11851,6 +11912,14 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
11851
11912
|
* even if the current client instance is idle/disconnected.
|
|
11852
11913
|
*/
|
|
11853
11914
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
11915
|
+
/**
|
|
11916
|
+
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
11917
|
+
*
|
|
11918
|
+
* Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
|
|
11919
|
+
* and creates a new one. Instance-level backoff variables would reset to zero,
|
|
11920
|
+
* allowing immediate reconnection and perpetuating the storm.
|
|
11921
|
+
*/
|
|
11922
|
+
static d2cDiscBackoff = /* @__PURE__ */ new Map();
|
|
11854
11923
|
/**
|
|
11855
11924
|
* Global (process-wide) CoverPreview serialization.
|
|
11856
11925
|
*
|
|
@@ -12546,7 +12615,12 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12546
12615
|
}
|
|
12547
12616
|
async waitForUdpReconnectCooldown() {
|
|
12548
12617
|
const now = Date.now();
|
|
12549
|
-
const
|
|
12618
|
+
const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
|
|
12619
|
+
const effectiveCooldownUntil = Math.max(
|
|
12620
|
+
this.udpReconnectCooldownUntilMs,
|
|
12621
|
+
staticEntry?.cooldownUntilMs ?? 0
|
|
12622
|
+
);
|
|
12623
|
+
const waitMs = effectiveCooldownUntil - now;
|
|
12550
12624
|
if (waitMs <= 0) return;
|
|
12551
12625
|
const sid = this.socketSessionId;
|
|
12552
12626
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -12555,7 +12629,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12555
12629
|
host: this.opts.host,
|
|
12556
12630
|
sid,
|
|
12557
12631
|
uid: shortUid,
|
|
12558
|
-
waitMs
|
|
12632
|
+
waitMs,
|
|
12633
|
+
persistent: staticEntry != null
|
|
12559
12634
|
});
|
|
12560
12635
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
12561
12636
|
}
|
|
@@ -12798,21 +12873,30 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12798
12873
|
uid: shortUid2,
|
|
12799
12874
|
message: err.message
|
|
12800
12875
|
});
|
|
12801
|
-
const
|
|
12876
|
+
const hostKey = this.opts.host;
|
|
12877
|
+
const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
|
|
12878
|
+
const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
|
|
12802
12879
|
const baseMs = 2e3;
|
|
12803
12880
|
const maxMs = 3e4;
|
|
12804
12881
|
const nextBackoffMs = withinWindow ? Math.min(
|
|
12805
12882
|
maxMs,
|
|
12806
12883
|
Math.max(
|
|
12807
12884
|
baseMs,
|
|
12808
|
-
|
|
12885
|
+
prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
|
|
12809
12886
|
)
|
|
12810
12887
|
) : baseMs;
|
|
12811
|
-
|
|
12812
|
-
|
|
12888
|
+
const cooldownUntilMs = Math.max(
|
|
12889
|
+
prev?.cooldownUntilMs ?? 0,
|
|
12890
|
+
now + nextBackoffMs
|
|
12891
|
+
);
|
|
12892
|
+
_BaichuanClient.d2cDiscBackoff.set(hostKey, {
|
|
12893
|
+
backoffMs: nextBackoffMs,
|
|
12894
|
+
lastAtMs: now,
|
|
12895
|
+
cooldownUntilMs
|
|
12896
|
+
});
|
|
12813
12897
|
this.udpReconnectCooldownUntilMs = Math.max(
|
|
12814
12898
|
this.udpReconnectCooldownUntilMs,
|
|
12815
|
-
|
|
12899
|
+
cooldownUntilMs
|
|
12816
12900
|
);
|
|
12817
12901
|
this.logDebug("d2c_disc_backoff", {
|
|
12818
12902
|
transport: "udp",
|
|
@@ -12820,7 +12904,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12820
12904
|
sid: sid2,
|
|
12821
12905
|
uid: shortUid2,
|
|
12822
12906
|
backoffMs: nextBackoffMs,
|
|
12823
|
-
cooldownUntilMs
|
|
12907
|
+
cooldownUntilMs,
|
|
12908
|
+
persistent: true
|
|
12824
12909
|
});
|
|
12825
12910
|
this.stopKeepAlive();
|
|
12826
12911
|
this.loggedIn = false;
|
|
@@ -12829,6 +12914,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12829
12914
|
this.videoSubscriptions.clear();
|
|
12830
12915
|
this.recomputeGlobalStreamingContribution();
|
|
12831
12916
|
}
|
|
12917
|
+
this.emit("d2c_disc", { host: this.opts.host, atMs: now });
|
|
12832
12918
|
}
|
|
12833
12919
|
this.emit("error", err);
|
|
12834
12920
|
});
|
|
@@ -13127,6 +13213,13 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
13127
13213
|
}
|
|
13128
13214
|
}
|
|
13129
13215
|
this.emit("push", frame);
|
|
13216
|
+
if (frame.header.cmdId === 252 && frame.body.length > 0) {
|
|
13217
|
+
try {
|
|
13218
|
+
this.emit("batteryPush", frame);
|
|
13219
|
+
} catch (error) {
|
|
13220
|
+
this.logDebug("battery_push_error", error);
|
|
13221
|
+
}
|
|
13222
|
+
}
|
|
13130
13223
|
if (frame.header.cmdId === 33) {
|
|
13131
13224
|
try {
|
|
13132
13225
|
const sid = this.socketSessionId;
|
|
@@ -17500,6 +17593,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17500
17593
|
}
|
|
17501
17594
|
}
|
|
17502
17595
|
const newClient = new BaichuanClient(this.clientOptions);
|
|
17596
|
+
this.attachD2cDiscListener(newClient);
|
|
17503
17597
|
this.socketPool.set("general", {
|
|
17504
17598
|
client: newClient,
|
|
17505
17599
|
refCount: 1,
|
|
@@ -17545,6 +17639,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17545
17639
|
if (!mapped) return;
|
|
17546
17640
|
this.dispatchSimpleEvent(mapped);
|
|
17547
17641
|
});
|
|
17642
|
+
client.on("batteryPush", (frame) => {
|
|
17643
|
+
try {
|
|
17644
|
+
const xml = this.client.tryDecryptXml(
|
|
17645
|
+
frame.body,
|
|
17646
|
+
frame.header.channelId,
|
|
17647
|
+
this.client.enc
|
|
17648
|
+
);
|
|
17649
|
+
if (!xml) return;
|
|
17650
|
+
const channel = frame.header.channelId;
|
|
17651
|
+
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
17652
|
+
if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
|
|
17653
|
+
this.dispatchSimpleEvent({
|
|
17654
|
+
type: "battery",
|
|
17655
|
+
channel,
|
|
17656
|
+
timestamp: Date.now(),
|
|
17657
|
+
battery
|
|
17658
|
+
});
|
|
17659
|
+
}
|
|
17660
|
+
} catch (e) {
|
|
17661
|
+
this.logger.debug?.(
|
|
17662
|
+
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
17663
|
+
formatErrorForLog(e)
|
|
17664
|
+
);
|
|
17665
|
+
}
|
|
17666
|
+
});
|
|
17548
17667
|
client.on("channelInfo", (xml) => {
|
|
17549
17668
|
try {
|
|
17550
17669
|
this.parseAndStoreChannelInfo(xml);
|
|
@@ -17680,6 +17799,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17680
17799
|
rtspServers = /* @__PURE__ */ new Set();
|
|
17681
17800
|
// Track all RTSP servers for cleanup
|
|
17682
17801
|
activeVideoMsgNums = /* @__PURE__ */ new Map();
|
|
17802
|
+
// ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
|
|
17803
|
+
// Tracked on the API instance (survives BaichuanClient recreation).
|
|
17804
|
+
/** Timestamp of the most recent D2C_DISC from any client for this device. */
|
|
17805
|
+
lastD2cDiscAtMs = 0;
|
|
17806
|
+
/** Sliding window of recent D2C_DISC timestamps for storm detection. */
|
|
17807
|
+
d2cDiscTimestamps = [];
|
|
17808
|
+
/** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
|
|
17809
|
+
* Prevents reconnect attempts while the camera is transitioning to sleep. */
|
|
17810
|
+
static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
|
|
17811
|
+
/** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
|
|
17812
|
+
static D2C_DISC_STORM_THRESHOLD = 3;
|
|
17813
|
+
/** Sliding window size (ms) for storm detection. */
|
|
17814
|
+
static D2C_DISC_STORM_WINDOW_MS = 6e4;
|
|
17815
|
+
/** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
|
|
17816
|
+
static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
|
|
17683
17817
|
nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
|
|
17684
17818
|
/**
|
|
17685
17819
|
* Cached device capabilities per channel.
|
|
@@ -18004,6 +18138,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18004
18138
|
const prefix = basename.substring(0, 10).toUpperCase();
|
|
18005
18139
|
return prefix.includes("S") ? "subStream" : "mainStream";
|
|
18006
18140
|
}
|
|
18141
|
+
/**
|
|
18142
|
+
* Stream profiles that the device explicitly rejected (response_code 400).
|
|
18143
|
+
* Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
|
|
18144
|
+
* it is excluded from `buildVideoStreamOptions()` results and no further
|
|
18145
|
+
* start attempts are made until the API instance is recreated.
|
|
18146
|
+
*/
|
|
18147
|
+
_rejectedStreamProfiles = /* @__PURE__ */ new Set();
|
|
18148
|
+
/**
|
|
18149
|
+
* Check whether a stream profile was rejected by the device at runtime
|
|
18150
|
+
* (e.g. ext returned response_code 400).
|
|
18151
|
+
*/
|
|
18152
|
+
isStreamProfileRejected(channel, profile) {
|
|
18153
|
+
return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
|
|
18154
|
+
}
|
|
18007
18155
|
/**
|
|
18008
18156
|
* Cache for buildVideoStreamOptions.
|
|
18009
18157
|
*
|
|
@@ -18093,6 +18241,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18093
18241
|
}
|
|
18094
18242
|
return "general";
|
|
18095
18243
|
}
|
|
18244
|
+
/**
|
|
18245
|
+
* Attach a D2C_DISC listener to a BaichuanClient so that the API-level
|
|
18246
|
+
* grace period and storm detection are updated regardless of which
|
|
18247
|
+
* pool socket receives the disconnect.
|
|
18248
|
+
*/
|
|
18249
|
+
attachD2cDiscListener(client) {
|
|
18250
|
+
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
18251
|
+
}
|
|
18096
18252
|
/**
|
|
18097
18253
|
* Acquire a socket from the pool by tag.
|
|
18098
18254
|
* Creates a new socket if needed, or reuses an existing one.
|
|
@@ -18113,10 +18269,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18113
18269
|
);
|
|
18114
18270
|
} else if (now < cooldownEntry.cooldownUntil) {
|
|
18115
18271
|
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
18272
|
+
const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
|
|
18273
|
+
const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
|
|
18116
18274
|
const error = new Error(
|
|
18117
|
-
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to
|
|
18275
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
|
|
18118
18276
|
);
|
|
18119
|
-
log?.
|
|
18277
|
+
log?.debug?.(error.message);
|
|
18120
18278
|
throw error;
|
|
18121
18279
|
}
|
|
18122
18280
|
}
|
|
@@ -18207,12 +18365,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18207
18365
|
try {
|
|
18208
18366
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18209
18367
|
const newClient = new BaichuanClient(clientOpts);
|
|
18368
|
+
this.attachD2cDiscListener(newClient);
|
|
18210
18369
|
await newClient.login();
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
)
|
|
18215
|
-
|
|
18370
|
+
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18371
|
+
if (existingCooldown) {
|
|
18372
|
+
const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
|
|
18373
|
+
if (!isStormCooldown) {
|
|
18374
|
+
log?.debug?.(
|
|
18375
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
18376
|
+
);
|
|
18377
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
18378
|
+
} else {
|
|
18379
|
+
log?.debug?.(
|
|
18380
|
+
`[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
|
|
18381
|
+
);
|
|
18382
|
+
}
|
|
18216
18383
|
}
|
|
18217
18384
|
entry.client = newClient;
|
|
18218
18385
|
entry.refCount = 1;
|
|
@@ -18514,6 +18681,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18514
18681
|
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
18515
18682
|
};
|
|
18516
18683
|
const generalClient = new BaichuanClient(opts);
|
|
18684
|
+
this.attachD2cDiscListener(generalClient);
|
|
18517
18685
|
this.socketPool.set("general", {
|
|
18518
18686
|
client: generalClient,
|
|
18519
18687
|
refCount: 1,
|
|
@@ -23108,6 +23276,16 @@ ${stderr}`)
|
|
|
23108
23276
|
}
|
|
23109
23277
|
if (!frame) frame = await targetClient.sendFrame(baseParams);
|
|
23110
23278
|
if (frame.header.responseCode !== 200) {
|
|
23279
|
+
if (frame.header.responseCode === 400) {
|
|
23280
|
+
const rejKey = `${ch}:${profile}`;
|
|
23281
|
+
if (!this._rejectedStreamProfiles.has(rejKey)) {
|
|
23282
|
+
this._rejectedStreamProfiles.add(rejKey);
|
|
23283
|
+
this.videoStreamOptionsCache.clear();
|
|
23284
|
+
this.logger?.warn?.(
|
|
23285
|
+
`[ReolinkBaichuanApi] Stream profile rejected by device: channel=${ch} profile=${profile} (response_code 400). This profile will be excluded from available streams. The camera may not support this stream profile with the current firmware.`
|
|
23286
|
+
);
|
|
23287
|
+
}
|
|
23288
|
+
}
|
|
23111
23289
|
throw new Error(
|
|
23112
23290
|
`Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
|
|
23113
23291
|
);
|
|
@@ -23578,6 +23756,49 @@ ${stderr}`)
|
|
|
23578
23756
|
if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
|
|
23579
23757
|
return out;
|
|
23580
23758
|
}
|
|
23759
|
+
/**
|
|
23760
|
+
* Called when any BaichuanClient for this device receives a D2C_DISC.
|
|
23761
|
+
*
|
|
23762
|
+
* Two-tier response:
|
|
23763
|
+
* 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
|
|
23764
|
+
* (10 s) to prevent reconnect attempts while the camera transitions to sleep.
|
|
23765
|
+
* 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
|
|
23766
|
+
*/
|
|
23767
|
+
notifyD2cDisc() {
|
|
23768
|
+
const now = Date.now();
|
|
23769
|
+
this.lastD2cDiscAtMs = now;
|
|
23770
|
+
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
23771
|
+
const existing = this.socketPoolCooldowns.get(this.host);
|
|
23772
|
+
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
23773
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
23774
|
+
failureCount: existing?.failureCount ?? 1,
|
|
23775
|
+
lastFailureAt: now,
|
|
23776
|
+
cooldownUntil: immediateCooldownUntil
|
|
23777
|
+
});
|
|
23778
|
+
this.logger?.log?.(
|
|
23779
|
+
`[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
|
|
23780
|
+
);
|
|
23781
|
+
}
|
|
23782
|
+
this.d2cDiscTimestamps.push(now);
|
|
23783
|
+
const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
|
|
23784
|
+
while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
|
|
23785
|
+
this.d2cDiscTimestamps.shift();
|
|
23786
|
+
}
|
|
23787
|
+
if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
|
|
23788
|
+
const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
|
|
23789
|
+
const currentEntry = this.socketPoolCooldowns.get(this.host);
|
|
23790
|
+
if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
|
|
23791
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
23792
|
+
failureCount: this.d2cDiscTimestamps.length,
|
|
23793
|
+
lastFailureAt: now,
|
|
23794
|
+
cooldownUntil: stormCooldownUntil
|
|
23795
|
+
});
|
|
23796
|
+
this.logger?.warn?.(
|
|
23797
|
+
`[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`
|
|
23798
|
+
);
|
|
23799
|
+
}
|
|
23800
|
+
}
|
|
23801
|
+
}
|
|
23581
23802
|
/**
|
|
23582
23803
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
23583
23804
|
*
|
|
@@ -23608,6 +23829,8 @@ ${stderr}`)
|
|
|
23608
23829
|
const socketConnected = this.client.isSocketConnected?.() ?? false;
|
|
23609
23830
|
const now = Date.now();
|
|
23610
23831
|
const cutoff = now - windowMs;
|
|
23832
|
+
const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
|
|
23833
|
+
const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
|
|
23611
23834
|
const rx = (this.client.getRxHistory?.() ?? []).filter(
|
|
23612
23835
|
(h) => h.atMs >= cutoff
|
|
23613
23836
|
);
|
|
@@ -23615,6 +23838,12 @@ ${stderr}`)
|
|
|
23615
23838
|
(h) => h.atMs >= cutoff
|
|
23616
23839
|
);
|
|
23617
23840
|
if (rx.length === 0 && tx.length === 0) {
|
|
23841
|
+
if (recentD2cDisc) {
|
|
23842
|
+
return {
|
|
23843
|
+
state: "sleeping",
|
|
23844
|
+
reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
23845
|
+
};
|
|
23846
|
+
}
|
|
23618
23847
|
return {
|
|
23619
23848
|
state: "sleeping",
|
|
23620
23849
|
reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
|
|
@@ -23638,6 +23867,12 @@ ${stderr}`)
|
|
|
23638
23867
|
idleMs: now - firstWakingTx.atMs
|
|
23639
23868
|
};
|
|
23640
23869
|
}
|
|
23870
|
+
if (recentD2cDisc) {
|
|
23871
|
+
return {
|
|
23872
|
+
state: "sleeping",
|
|
23873
|
+
reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
23874
|
+
};
|
|
23875
|
+
}
|
|
23641
23876
|
return {
|
|
23642
23877
|
state: "sleeping",
|
|
23643
23878
|
reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
|
|
@@ -25072,6 +25307,8 @@ ${xml}`
|
|
|
25072
25307
|
for (const metadata of params.metadatas) {
|
|
25073
25308
|
const profile = metadata.profile;
|
|
25074
25309
|
if (isMultiFocal && profile === "ext") continue;
|
|
25310
|
+
if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
|
|
25311
|
+
continue;
|
|
25075
25312
|
if (params.includeRtsp && profile !== "ext") {
|
|
25076
25313
|
const streamName = profile === "main" ? "main" : "sub";
|
|
25077
25314
|
pushRtsp({
|