@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.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
BaichuanRtspServer,
|
|
4
4
|
ReolinkBaichuanApi,
|
|
5
5
|
autoDetectDeviceType
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-F2Y5U3YP.js";
|
|
7
7
|
import {
|
|
8
8
|
__require
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-TR3V5FTO.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/rtsp-server.ts
|
|
12
12
|
function parseArgs() {
|
package/dist/index.cjs
CHANGED
|
@@ -2178,6 +2178,19 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2178
2178
|
// Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
|
|
2179
2179
|
// In CFB mode, continuation frames must use the cipher state from previous frames.
|
|
2180
2180
|
aesStreamDecryptor = null;
|
|
2181
|
+
/**
|
|
2182
|
+
* Pending startup error stashed when emitSafeError is called before any
|
|
2183
|
+
* "error" listener is registered (e.g. camera returns 400 during start()).
|
|
2184
|
+
* The rfc4571-server's waitForKeyframe can consume this immediately instead
|
|
2185
|
+
* of waiting for the full keyframe timeout.
|
|
2186
|
+
*/
|
|
2187
|
+
_pendingStartupError;
|
|
2188
|
+
/** Consume and clear any pending startup error. */
|
|
2189
|
+
consumePendingStartupError() {
|
|
2190
|
+
const err = this._pendingStartupError;
|
|
2191
|
+
this._pendingStartupError = void 0;
|
|
2192
|
+
return err;
|
|
2193
|
+
}
|
|
2181
2194
|
emitSafeError(err) {
|
|
2182
2195
|
if (!this.active) {
|
|
2183
2196
|
this.logger?.warn?.(
|
|
@@ -2189,6 +2202,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2189
2202
|
this.logger?.warn?.(
|
|
2190
2203
|
`[BaichuanVideoStream] Unhandled stream error: ${err.message}`
|
|
2191
2204
|
);
|
|
2205
|
+
this._pendingStartupError = err;
|
|
2192
2206
|
return;
|
|
2193
2207
|
}
|
|
2194
2208
|
this.emit("error", err);
|
|
@@ -8527,6 +8541,9 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
8527
8541
|
resendTimer;
|
|
8528
8542
|
hbTimer;
|
|
8529
8543
|
discoveryTid;
|
|
8544
|
+
// Track discovery-phase timers so close() can cancel them even if
|
|
8545
|
+
// discovery is still in progress (prevents ERR_SOCKET_DGRAM_NOT_RUNNING).
|
|
8546
|
+
discoveryTimers = [];
|
|
8530
8547
|
acceptSent = false;
|
|
8531
8548
|
lastAcceptAtMs;
|
|
8532
8549
|
ackScheduled = false;
|
|
@@ -8575,9 +8592,31 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
8575
8592
|
});
|
|
8576
8593
|
sock.on("error", (e) => this.emit("error", e));
|
|
8577
8594
|
sock.on("close", () => this.emit("close"));
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8595
|
+
const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
|
|
8596
|
+
for (let i = portRange.length - 1; i > 0; i--) {
|
|
8597
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
8598
|
+
[portRange[i], portRange[j]] = [portRange[j], portRange[i]];
|
|
8599
|
+
}
|
|
8600
|
+
let bound = false;
|
|
8601
|
+
for (const port of portRange) {
|
|
8602
|
+
try {
|
|
8603
|
+
await new Promise((resolve, reject) => {
|
|
8604
|
+
sock.once("error", reject);
|
|
8605
|
+
sock.bind(port, "0.0.0.0", () => {
|
|
8606
|
+
sock.removeListener("error", reject);
|
|
8607
|
+
resolve();
|
|
8608
|
+
});
|
|
8609
|
+
});
|
|
8610
|
+
bound = true;
|
|
8611
|
+
break;
|
|
8612
|
+
} catch {
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
if (!bound) {
|
|
8616
|
+
await new Promise(
|
|
8617
|
+
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
8618
|
+
);
|
|
8619
|
+
}
|
|
8581
8620
|
if (this.opts.mode === "direct") {
|
|
8582
8621
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
8583
8622
|
this.clientId = this.opts.clientId;
|
|
@@ -8997,7 +9036,24 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
8997
9036
|
BCUDP_DISCOVERY_PORT_LOCAL_ANY,
|
|
8998
9037
|
BCUDP_DISCOVERY_PORT_LOCAL_UID
|
|
8999
9038
|
];
|
|
9000
|
-
const
|
|
9039
|
+
const broadcastHosts = ["255.255.255.255"];
|
|
9040
|
+
const ifaces = (0, import_node_os.networkInterfaces)();
|
|
9041
|
+
for (const name of Object.keys(ifaces)) {
|
|
9042
|
+
const entries = ifaces[name];
|
|
9043
|
+
if (!entries) continue;
|
|
9044
|
+
for (const addr2 of entries) {
|
|
9045
|
+
if (addr2.family === "IPv4" && !addr2.internal && addr2.cidr) {
|
|
9046
|
+
const ipParts = addr2.address.split(".").map(Number);
|
|
9047
|
+
const maskParts = addr2.netmask.split(".").map(Number);
|
|
9048
|
+
if (ipParts.length === 4 && maskParts.length === 4) {
|
|
9049
|
+
const bcast = ipParts.map((octet, i) => octet | ~maskParts[i] & 255).join(".");
|
|
9050
|
+
if (!broadcastHosts.includes(bcast)) {
|
|
9051
|
+
broadcastHosts.push(bcast);
|
|
9052
|
+
}
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
}
|
|
9056
|
+
}
|
|
9001
9057
|
const directHost = (this.opts.directHost ?? "").trim();
|
|
9002
9058
|
const localMode = opts?.localMode ?? "local-broadcast";
|
|
9003
9059
|
const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
|
|
@@ -9024,6 +9080,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9024
9080
|
)
|
|
9025
9081
|
);
|
|
9026
9082
|
}, discoveryTimeout);
|
|
9083
|
+
this.discoveryTimers.push(timeout);
|
|
9027
9084
|
let retryTimer;
|
|
9028
9085
|
let retryCount = 0;
|
|
9029
9086
|
let discoveredSid;
|
|
@@ -9200,11 +9257,11 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9200
9257
|
if (directHost) {
|
|
9201
9258
|
if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
|
|
9202
9259
|
return [directHost];
|
|
9203
|
-
return [directHost,
|
|
9260
|
+
return [directHost, ...broadcastHosts];
|
|
9204
9261
|
}
|
|
9205
|
-
return
|
|
9262
|
+
return broadcastHosts;
|
|
9206
9263
|
}
|
|
9207
|
-
return
|
|
9264
|
+
return broadcastHosts;
|
|
9208
9265
|
})();
|
|
9209
9266
|
for (const host of Array.from(new Set(hosts))) {
|
|
9210
9267
|
for (const port of ports) {
|
|
@@ -9212,8 +9269,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9212
9269
|
sock.send(packet, port, host);
|
|
9213
9270
|
retryCount++;
|
|
9214
9271
|
this.emit("debug", "discovery_send", { retryCount, host, port });
|
|
9215
|
-
} catch
|
|
9216
|
-
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
9272
|
+
} catch {
|
|
9217
9273
|
}
|
|
9218
9274
|
}
|
|
9219
9275
|
}
|
|
@@ -9222,6 +9278,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9222
9278
|
retryTimer = (0, import_node_timers.setInterval)(() => {
|
|
9223
9279
|
sendDiscovery();
|
|
9224
9280
|
}, retryInterval);
|
|
9281
|
+
this.discoveryTimers.push(retryTimer);
|
|
9225
9282
|
});
|
|
9226
9283
|
this.clientId = reply.cid;
|
|
9227
9284
|
this.cameraId = reply.did;
|
|
@@ -9538,6 +9595,10 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9538
9595
|
this.ackTimer = void 0;
|
|
9539
9596
|
this.resendTimer = void 0;
|
|
9540
9597
|
this.hbTimer = void 0;
|
|
9598
|
+
for (const t of this.discoveryTimers) {
|
|
9599
|
+
clearInterval(t);
|
|
9600
|
+
}
|
|
9601
|
+
this.discoveryTimers = [];
|
|
9541
9602
|
const s = this.sock;
|
|
9542
9603
|
this.sock = void 0;
|
|
9543
9604
|
if (!s) return;
|
|
@@ -9682,6 +9743,14 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
9682
9743
|
* even if the current client instance is idle/disconnected.
|
|
9683
9744
|
*/
|
|
9684
9745
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
9746
|
+
/**
|
|
9747
|
+
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
9748
|
+
*
|
|
9749
|
+
* Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
|
|
9750
|
+
* and creates a new one. Instance-level backoff variables would reset to zero,
|
|
9751
|
+
* allowing immediate reconnection and perpetuating the storm.
|
|
9752
|
+
*/
|
|
9753
|
+
static d2cDiscBackoff = /* @__PURE__ */ new Map();
|
|
9685
9754
|
/**
|
|
9686
9755
|
* Global (process-wide) CoverPreview serialization.
|
|
9687
9756
|
*
|
|
@@ -10377,7 +10446,12 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10377
10446
|
}
|
|
10378
10447
|
async waitForUdpReconnectCooldown() {
|
|
10379
10448
|
const now = Date.now();
|
|
10380
|
-
const
|
|
10449
|
+
const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
|
|
10450
|
+
const effectiveCooldownUntil = Math.max(
|
|
10451
|
+
this.udpReconnectCooldownUntilMs,
|
|
10452
|
+
staticEntry?.cooldownUntilMs ?? 0
|
|
10453
|
+
);
|
|
10454
|
+
const waitMs = effectiveCooldownUntil - now;
|
|
10381
10455
|
if (waitMs <= 0) return;
|
|
10382
10456
|
const sid = this.socketSessionId;
|
|
10383
10457
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -10386,7 +10460,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10386
10460
|
host: this.opts.host,
|
|
10387
10461
|
sid,
|
|
10388
10462
|
uid: shortUid,
|
|
10389
|
-
waitMs
|
|
10463
|
+
waitMs,
|
|
10464
|
+
persistent: staticEntry != null
|
|
10390
10465
|
});
|
|
10391
10466
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
10392
10467
|
}
|
|
@@ -10629,21 +10704,30 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10629
10704
|
uid: shortUid2,
|
|
10630
10705
|
message: err.message
|
|
10631
10706
|
});
|
|
10632
|
-
const
|
|
10707
|
+
const hostKey = this.opts.host;
|
|
10708
|
+
const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
|
|
10709
|
+
const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
|
|
10633
10710
|
const baseMs = 2e3;
|
|
10634
10711
|
const maxMs = 3e4;
|
|
10635
10712
|
const nextBackoffMs = withinWindow ? Math.min(
|
|
10636
10713
|
maxMs,
|
|
10637
10714
|
Math.max(
|
|
10638
10715
|
baseMs,
|
|
10639
|
-
|
|
10716
|
+
prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
|
|
10640
10717
|
)
|
|
10641
10718
|
) : baseMs;
|
|
10642
|
-
|
|
10643
|
-
|
|
10719
|
+
const cooldownUntilMs = Math.max(
|
|
10720
|
+
prev?.cooldownUntilMs ?? 0,
|
|
10721
|
+
now + nextBackoffMs
|
|
10722
|
+
);
|
|
10723
|
+
_BaichuanClient.d2cDiscBackoff.set(hostKey, {
|
|
10724
|
+
backoffMs: nextBackoffMs,
|
|
10725
|
+
lastAtMs: now,
|
|
10726
|
+
cooldownUntilMs
|
|
10727
|
+
});
|
|
10644
10728
|
this.udpReconnectCooldownUntilMs = Math.max(
|
|
10645
10729
|
this.udpReconnectCooldownUntilMs,
|
|
10646
|
-
|
|
10730
|
+
cooldownUntilMs
|
|
10647
10731
|
);
|
|
10648
10732
|
this.logDebug("d2c_disc_backoff", {
|
|
10649
10733
|
transport: "udp",
|
|
@@ -10651,7 +10735,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10651
10735
|
sid: sid2,
|
|
10652
10736
|
uid: shortUid2,
|
|
10653
10737
|
backoffMs: nextBackoffMs,
|
|
10654
|
-
cooldownUntilMs
|
|
10738
|
+
cooldownUntilMs,
|
|
10739
|
+
persistent: true
|
|
10655
10740
|
});
|
|
10656
10741
|
this.stopKeepAlive();
|
|
10657
10742
|
this.loggedIn = false;
|
|
@@ -10660,6 +10745,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10660
10745
|
this.videoSubscriptions.clear();
|
|
10661
10746
|
this.recomputeGlobalStreamingContribution();
|
|
10662
10747
|
}
|
|
10748
|
+
this.emit("d2c_disc", { host: this.opts.host, atMs: now });
|
|
10663
10749
|
}
|
|
10664
10750
|
this.emit("error", err);
|
|
10665
10751
|
});
|
|
@@ -10958,6 +11044,13 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10958
11044
|
}
|
|
10959
11045
|
}
|
|
10960
11046
|
this.emit("push", frame);
|
|
11047
|
+
if (frame.header.cmdId === 252 && frame.body.length > 0) {
|
|
11048
|
+
try {
|
|
11049
|
+
this.emit("batteryPush", frame);
|
|
11050
|
+
} catch (error) {
|
|
11051
|
+
this.logDebug("battery_push_error", error);
|
|
11052
|
+
}
|
|
11053
|
+
}
|
|
10961
11054
|
if (frame.header.cmdId === 33) {
|
|
10962
11055
|
try {
|
|
10963
11056
|
const sid = this.socketSessionId;
|
|
@@ -18077,6 +18170,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18077
18170
|
}
|
|
18078
18171
|
}
|
|
18079
18172
|
const newClient = new BaichuanClient(this.clientOptions);
|
|
18173
|
+
this.attachD2cDiscListener(newClient);
|
|
18080
18174
|
this.socketPool.set("general", {
|
|
18081
18175
|
client: newClient,
|
|
18082
18176
|
refCount: 1,
|
|
@@ -18122,6 +18216,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18122
18216
|
if (!mapped) return;
|
|
18123
18217
|
this.dispatchSimpleEvent(mapped);
|
|
18124
18218
|
});
|
|
18219
|
+
client.on("batteryPush", (frame) => {
|
|
18220
|
+
try {
|
|
18221
|
+
const xml = this.client.tryDecryptXml(
|
|
18222
|
+
frame.body,
|
|
18223
|
+
frame.header.channelId,
|
|
18224
|
+
this.client.enc
|
|
18225
|
+
);
|
|
18226
|
+
if (!xml) return;
|
|
18227
|
+
const channel = frame.header.channelId;
|
|
18228
|
+
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
18229
|
+
if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
|
|
18230
|
+
this.dispatchSimpleEvent({
|
|
18231
|
+
type: "battery",
|
|
18232
|
+
channel,
|
|
18233
|
+
timestamp: Date.now(),
|
|
18234
|
+
battery
|
|
18235
|
+
});
|
|
18236
|
+
}
|
|
18237
|
+
} catch (e) {
|
|
18238
|
+
this.logger.debug?.(
|
|
18239
|
+
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
18240
|
+
formatErrorForLog(e)
|
|
18241
|
+
);
|
|
18242
|
+
}
|
|
18243
|
+
});
|
|
18125
18244
|
client.on("channelInfo", (xml) => {
|
|
18126
18245
|
try {
|
|
18127
18246
|
this.parseAndStoreChannelInfo(xml);
|
|
@@ -18257,6 +18376,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18257
18376
|
rtspServers = /* @__PURE__ */ new Set();
|
|
18258
18377
|
// Track all RTSP servers for cleanup
|
|
18259
18378
|
activeVideoMsgNums = /* @__PURE__ */ new Map();
|
|
18379
|
+
// ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
|
|
18380
|
+
// Tracked on the API instance (survives BaichuanClient recreation).
|
|
18381
|
+
/** Timestamp of the most recent D2C_DISC from any client for this device. */
|
|
18382
|
+
lastD2cDiscAtMs = 0;
|
|
18383
|
+
/** Sliding window of recent D2C_DISC timestamps for storm detection. */
|
|
18384
|
+
d2cDiscTimestamps = [];
|
|
18385
|
+
/** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
|
|
18386
|
+
* Prevents reconnect attempts while the camera is transitioning to sleep. */
|
|
18387
|
+
static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
|
|
18388
|
+
/** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
|
|
18389
|
+
static D2C_DISC_STORM_THRESHOLD = 3;
|
|
18390
|
+
/** Sliding window size (ms) for storm detection. */
|
|
18391
|
+
static D2C_DISC_STORM_WINDOW_MS = 6e4;
|
|
18392
|
+
/** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
|
|
18393
|
+
static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
|
|
18260
18394
|
nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
|
|
18261
18395
|
/**
|
|
18262
18396
|
* Cached device capabilities per channel.
|
|
@@ -18581,6 +18715,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18581
18715
|
const prefix = basename.substring(0, 10).toUpperCase();
|
|
18582
18716
|
return prefix.includes("S") ? "subStream" : "mainStream";
|
|
18583
18717
|
}
|
|
18718
|
+
/**
|
|
18719
|
+
* Stream profiles that the device explicitly rejected (response_code 400).
|
|
18720
|
+
* Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
|
|
18721
|
+
* it is excluded from `buildVideoStreamOptions()` results and no further
|
|
18722
|
+
* start attempts are made until the API instance is recreated.
|
|
18723
|
+
*/
|
|
18724
|
+
_rejectedStreamProfiles = /* @__PURE__ */ new Set();
|
|
18725
|
+
/**
|
|
18726
|
+
* Check whether a stream profile was rejected by the device at runtime
|
|
18727
|
+
* (e.g. ext returned response_code 400).
|
|
18728
|
+
*/
|
|
18729
|
+
isStreamProfileRejected(channel, profile) {
|
|
18730
|
+
return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
|
|
18731
|
+
}
|
|
18584
18732
|
/**
|
|
18585
18733
|
* Cache for buildVideoStreamOptions.
|
|
18586
18734
|
*
|
|
@@ -18670,6 +18818,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18670
18818
|
}
|
|
18671
18819
|
return "general";
|
|
18672
18820
|
}
|
|
18821
|
+
/**
|
|
18822
|
+
* Attach a D2C_DISC listener to a BaichuanClient so that the API-level
|
|
18823
|
+
* grace period and storm detection are updated regardless of which
|
|
18824
|
+
* pool socket receives the disconnect.
|
|
18825
|
+
*/
|
|
18826
|
+
attachD2cDiscListener(client) {
|
|
18827
|
+
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
18828
|
+
}
|
|
18673
18829
|
/**
|
|
18674
18830
|
* Acquire a socket from the pool by tag.
|
|
18675
18831
|
* Creates a new socket if needed, or reuses an existing one.
|
|
@@ -18690,10 +18846,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18690
18846
|
);
|
|
18691
18847
|
} else if (now < cooldownEntry.cooldownUntil) {
|
|
18692
18848
|
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
18849
|
+
const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
|
|
18850
|
+
const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
|
|
18693
18851
|
const error = new Error(
|
|
18694
|
-
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to
|
|
18852
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
|
|
18695
18853
|
);
|
|
18696
|
-
log?.
|
|
18854
|
+
log?.debug?.(error.message);
|
|
18697
18855
|
throw error;
|
|
18698
18856
|
}
|
|
18699
18857
|
}
|
|
@@ -18784,12 +18942,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18784
18942
|
try {
|
|
18785
18943
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18786
18944
|
const newClient = new BaichuanClient(clientOpts);
|
|
18945
|
+
this.attachD2cDiscListener(newClient);
|
|
18787
18946
|
await newClient.login();
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
)
|
|
18792
|
-
|
|
18947
|
+
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18948
|
+
if (existingCooldown) {
|
|
18949
|
+
const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
|
|
18950
|
+
if (!isStormCooldown) {
|
|
18951
|
+
log?.debug?.(
|
|
18952
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
18953
|
+
);
|
|
18954
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
18955
|
+
} else {
|
|
18956
|
+
log?.debug?.(
|
|
18957
|
+
`[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
|
|
18958
|
+
);
|
|
18959
|
+
}
|
|
18793
18960
|
}
|
|
18794
18961
|
entry.client = newClient;
|
|
18795
18962
|
entry.refCount = 1;
|
|
@@ -19091,6 +19258,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19091
19258
|
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
19092
19259
|
};
|
|
19093
19260
|
const generalClient = new BaichuanClient(opts);
|
|
19261
|
+
this.attachD2cDiscListener(generalClient);
|
|
19094
19262
|
this.socketPool.set("general", {
|
|
19095
19263
|
client: generalClient,
|
|
19096
19264
|
refCount: 1,
|
|
@@ -23685,6 +23853,16 @@ ${stderr}`)
|
|
|
23685
23853
|
}
|
|
23686
23854
|
if (!frame) frame = await targetClient.sendFrame(baseParams);
|
|
23687
23855
|
if (frame.header.responseCode !== 200) {
|
|
23856
|
+
if (frame.header.responseCode === 400) {
|
|
23857
|
+
const rejKey = `${ch}:${profile}`;
|
|
23858
|
+
if (!this._rejectedStreamProfiles.has(rejKey)) {
|
|
23859
|
+
this._rejectedStreamProfiles.add(rejKey);
|
|
23860
|
+
this.videoStreamOptionsCache.clear();
|
|
23861
|
+
this.logger?.warn?.(
|
|
23862
|
+
`[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.`
|
|
23863
|
+
);
|
|
23864
|
+
}
|
|
23865
|
+
}
|
|
23688
23866
|
throw new Error(
|
|
23689
23867
|
`Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
|
|
23690
23868
|
);
|
|
@@ -24155,6 +24333,49 @@ ${stderr}`)
|
|
|
24155
24333
|
if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
|
|
24156
24334
|
return out;
|
|
24157
24335
|
}
|
|
24336
|
+
/**
|
|
24337
|
+
* Called when any BaichuanClient for this device receives a D2C_DISC.
|
|
24338
|
+
*
|
|
24339
|
+
* Two-tier response:
|
|
24340
|
+
* 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
|
|
24341
|
+
* (10 s) to prevent reconnect attempts while the camera transitions to sleep.
|
|
24342
|
+
* 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
|
|
24343
|
+
*/
|
|
24344
|
+
notifyD2cDisc() {
|
|
24345
|
+
const now = Date.now();
|
|
24346
|
+
this.lastD2cDiscAtMs = now;
|
|
24347
|
+
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
24348
|
+
const existing = this.socketPoolCooldowns.get(this.host);
|
|
24349
|
+
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
24350
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
24351
|
+
failureCount: existing?.failureCount ?? 1,
|
|
24352
|
+
lastFailureAt: now,
|
|
24353
|
+
cooldownUntil: immediateCooldownUntil
|
|
24354
|
+
});
|
|
24355
|
+
this.logger?.log?.(
|
|
24356
|
+
`[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
|
|
24357
|
+
);
|
|
24358
|
+
}
|
|
24359
|
+
this.d2cDiscTimestamps.push(now);
|
|
24360
|
+
const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
|
|
24361
|
+
while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
|
|
24362
|
+
this.d2cDiscTimestamps.shift();
|
|
24363
|
+
}
|
|
24364
|
+
if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
|
|
24365
|
+
const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
|
|
24366
|
+
const currentEntry = this.socketPoolCooldowns.get(this.host);
|
|
24367
|
+
if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
|
|
24368
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
24369
|
+
failureCount: this.d2cDiscTimestamps.length,
|
|
24370
|
+
lastFailureAt: now,
|
|
24371
|
+
cooldownUntil: stormCooldownUntil
|
|
24372
|
+
});
|
|
24373
|
+
this.logger?.warn?.(
|
|
24374
|
+
`[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`
|
|
24375
|
+
);
|
|
24376
|
+
}
|
|
24377
|
+
}
|
|
24378
|
+
}
|
|
24158
24379
|
/**
|
|
24159
24380
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
24160
24381
|
*
|
|
@@ -24185,6 +24406,8 @@ ${stderr}`)
|
|
|
24185
24406
|
const socketConnected = this.client.isSocketConnected?.() ?? false;
|
|
24186
24407
|
const now = Date.now();
|
|
24187
24408
|
const cutoff = now - windowMs;
|
|
24409
|
+
const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
|
|
24410
|
+
const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
|
|
24188
24411
|
const rx = (this.client.getRxHistory?.() ?? []).filter(
|
|
24189
24412
|
(h) => h.atMs >= cutoff
|
|
24190
24413
|
);
|
|
@@ -24192,6 +24415,12 @@ ${stderr}`)
|
|
|
24192
24415
|
(h) => h.atMs >= cutoff
|
|
24193
24416
|
);
|
|
24194
24417
|
if (rx.length === 0 && tx.length === 0) {
|
|
24418
|
+
if (recentD2cDisc) {
|
|
24419
|
+
return {
|
|
24420
|
+
state: "sleeping",
|
|
24421
|
+
reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
24422
|
+
};
|
|
24423
|
+
}
|
|
24195
24424
|
return {
|
|
24196
24425
|
state: "sleeping",
|
|
24197
24426
|
reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
|
|
@@ -24215,6 +24444,12 @@ ${stderr}`)
|
|
|
24215
24444
|
idleMs: now - firstWakingTx.atMs
|
|
24216
24445
|
};
|
|
24217
24446
|
}
|
|
24447
|
+
if (recentD2cDisc) {
|
|
24448
|
+
return {
|
|
24449
|
+
state: "sleeping",
|
|
24450
|
+
reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
24451
|
+
};
|
|
24452
|
+
}
|
|
24218
24453
|
return {
|
|
24219
24454
|
state: "sleeping",
|
|
24220
24455
|
reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
|
|
@@ -25649,6 +25884,8 @@ ${xml}`
|
|
|
25649
25884
|
for (const metadata of params.metadatas) {
|
|
25650
25885
|
const profile = metadata.profile;
|
|
25651
25886
|
if (isMultiFocal && profile === "ext") continue;
|
|
25887
|
+
if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
|
|
25888
|
+
continue;
|
|
25652
25889
|
if (params.includeRtsp && profile !== "ext") {
|
|
25653
25890
|
const streamName = profile === "main" ? "main" : "sub";
|
|
25654
25891
|
pushRtsp({
|
|
@@ -32139,6 +32376,11 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
32139
32376
|
"videoAccessUnit",
|
|
32140
32377
|
onAu
|
|
32141
32378
|
);
|
|
32379
|
+
const pendingErr = videoStream.consumePendingStartupError?.();
|
|
32380
|
+
if (pendingErr) {
|
|
32381
|
+
cleanup();
|
|
32382
|
+
reject(pendingErr);
|
|
32383
|
+
}
|
|
32142
32384
|
});
|
|
32143
32385
|
}
|
|
32144
32386
|
};
|
|
@@ -32150,24 +32392,32 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
32150
32392
|
await videoStream.stop();
|
|
32151
32393
|
} catch {
|
|
32152
32394
|
}
|
|
32153
|
-
if (
|
|
32154
|
-
|
|
32155
|
-
|
|
32395
|
+
if (dedicatedSession) {
|
|
32396
|
+
try {
|
|
32397
|
+
await dedicatedSession.release();
|
|
32398
|
+
} catch {
|
|
32399
|
+
}
|
|
32400
|
+
}
|
|
32401
|
+
if (!dedicatedSession) {
|
|
32402
|
+
if (closeApiOnTeardown) {
|
|
32403
|
+
await Promise.allSettled(
|
|
32404
|
+
Array.from(apisToClose).map(async (a) => {
|
|
32405
|
+
try {
|
|
32406
|
+
await a.close();
|
|
32407
|
+
} catch {
|
|
32408
|
+
}
|
|
32409
|
+
})
|
|
32410
|
+
);
|
|
32411
|
+
} else {
|
|
32412
|
+
const graceMs = isComposite ? 5e3 : 0;
|
|
32413
|
+
for (const a of Array.from(apisToClose)) {
|
|
32156
32414
|
try {
|
|
32157
|
-
|
|
32415
|
+
a?.client?.requestIdleDisconnectSoon?.(
|
|
32416
|
+
"rfc4571_teardown",
|
|
32417
|
+
graceMs
|
|
32418
|
+
);
|
|
32158
32419
|
} catch {
|
|
32159
32420
|
}
|
|
32160
|
-
})
|
|
32161
|
-
);
|
|
32162
|
-
} else {
|
|
32163
|
-
const graceMs = isComposite ? 5e3 : 0;
|
|
32164
|
-
for (const a of Array.from(apisToClose)) {
|
|
32165
|
-
try {
|
|
32166
|
-
a?.client?.requestIdleDisconnectSoon?.(
|
|
32167
|
-
"rfc4571_teardown",
|
|
32168
|
-
graceMs
|
|
32169
|
-
);
|
|
32170
|
-
} catch {
|
|
32171
32421
|
}
|
|
32172
32422
|
}
|
|
32173
32423
|
}
|
|
@@ -32423,7 +32673,7 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
32423
32673
|
} catch {
|
|
32424
32674
|
}
|
|
32425
32675
|
}
|
|
32426
|
-
if (closeApiOnTeardown) {
|
|
32676
|
+
if (closeApiOnTeardown && !dedicatedSession) {
|
|
32427
32677
|
await Promise.allSettled(
|
|
32428
32678
|
Array.from(apisToClose).map(async (a) => {
|
|
32429
32679
|
try {
|