@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/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 |
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
sampleStreams,
|
|
13
13
|
sanitizeFixtureData,
|
|
14
14
|
testChannelStreams
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-TR3V5FTO.js";
|
|
16
16
|
export {
|
|
17
17
|
captureModelFixtures,
|
|
18
18
|
collectCgiDiagnostics,
|
|
@@ -28,4 +28,4 @@ export {
|
|
|
28
28
|
sanitizeFixtureData,
|
|
29
29
|
testChannelStreams
|
|
30
30
|
};
|
|
31
|
-
//# sourceMappingURL=DiagnosticsTools-
|
|
31
|
+
//# sourceMappingURL=DiagnosticsTools-UMN4C7SY.js.map
|
|
@@ -144,7 +144,7 @@ import {
|
|
|
144
144
|
talkTraceLog,
|
|
145
145
|
traceLog,
|
|
146
146
|
xmlEscape
|
|
147
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-TR3V5FTO.js";
|
|
148
148
|
|
|
149
149
|
// src/protocol/framing.ts
|
|
150
150
|
function encodeHeader(h) {
|
|
@@ -654,6 +654,9 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
654
654
|
resendTimer;
|
|
655
655
|
hbTimer;
|
|
656
656
|
discoveryTid;
|
|
657
|
+
// Track discovery-phase timers so close() can cancel them even if
|
|
658
|
+
// discovery is still in progress (prevents ERR_SOCKET_DGRAM_NOT_RUNNING).
|
|
659
|
+
discoveryTimers = [];
|
|
657
660
|
acceptSent = false;
|
|
658
661
|
lastAcceptAtMs;
|
|
659
662
|
ackScheduled = false;
|
|
@@ -702,9 +705,31 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
702
705
|
});
|
|
703
706
|
sock.on("error", (e) => this.emit("error", e));
|
|
704
707
|
sock.on("close", () => this.emit("close"));
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
+
const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
|
|
709
|
+
for (let i = portRange.length - 1; i > 0; i--) {
|
|
710
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
711
|
+
[portRange[i], portRange[j]] = [portRange[j], portRange[i]];
|
|
712
|
+
}
|
|
713
|
+
let bound = false;
|
|
714
|
+
for (const port of portRange) {
|
|
715
|
+
try {
|
|
716
|
+
await new Promise((resolve, reject) => {
|
|
717
|
+
sock.once("error", reject);
|
|
718
|
+
sock.bind(port, "0.0.0.0", () => {
|
|
719
|
+
sock.removeListener("error", reject);
|
|
720
|
+
resolve();
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
bound = true;
|
|
724
|
+
break;
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!bound) {
|
|
729
|
+
await new Promise(
|
|
730
|
+
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
731
|
+
);
|
|
732
|
+
}
|
|
708
733
|
if (this.opts.mode === "direct") {
|
|
709
734
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
710
735
|
this.clientId = this.opts.clientId;
|
|
@@ -1124,7 +1149,24 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1124
1149
|
BCUDP_DISCOVERY_PORT_LOCAL_ANY,
|
|
1125
1150
|
BCUDP_DISCOVERY_PORT_LOCAL_UID
|
|
1126
1151
|
];
|
|
1127
|
-
const
|
|
1152
|
+
const broadcastHosts = ["255.255.255.255"];
|
|
1153
|
+
const ifaces = networkInterfaces();
|
|
1154
|
+
for (const name of Object.keys(ifaces)) {
|
|
1155
|
+
const entries = ifaces[name];
|
|
1156
|
+
if (!entries) continue;
|
|
1157
|
+
for (const addr2 of entries) {
|
|
1158
|
+
if (addr2.family === "IPv4" && !addr2.internal && addr2.cidr) {
|
|
1159
|
+
const ipParts = addr2.address.split(".").map(Number);
|
|
1160
|
+
const maskParts = addr2.netmask.split(".").map(Number);
|
|
1161
|
+
if (ipParts.length === 4 && maskParts.length === 4) {
|
|
1162
|
+
const bcast = ipParts.map((octet, i) => octet | ~maskParts[i] & 255).join(".");
|
|
1163
|
+
if (!broadcastHosts.includes(bcast)) {
|
|
1164
|
+
broadcastHosts.push(bcast);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1128
1170
|
const directHost = (this.opts.directHost ?? "").trim();
|
|
1129
1171
|
const localMode = opts?.localMode ?? "local-broadcast";
|
|
1130
1172
|
const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
|
|
@@ -1151,6 +1193,7 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1151
1193
|
)
|
|
1152
1194
|
);
|
|
1153
1195
|
}, discoveryTimeout);
|
|
1196
|
+
this.discoveryTimers.push(timeout);
|
|
1154
1197
|
let retryTimer;
|
|
1155
1198
|
let retryCount = 0;
|
|
1156
1199
|
let discoveredSid;
|
|
@@ -1327,11 +1370,11 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1327
1370
|
if (directHost) {
|
|
1328
1371
|
if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
|
|
1329
1372
|
return [directHost];
|
|
1330
|
-
return [directHost,
|
|
1373
|
+
return [directHost, ...broadcastHosts];
|
|
1331
1374
|
}
|
|
1332
|
-
return
|
|
1375
|
+
return broadcastHosts;
|
|
1333
1376
|
}
|
|
1334
|
-
return
|
|
1377
|
+
return broadcastHosts;
|
|
1335
1378
|
})();
|
|
1336
1379
|
for (const host of Array.from(new Set(hosts))) {
|
|
1337
1380
|
for (const port of ports) {
|
|
@@ -1339,8 +1382,7 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1339
1382
|
sock.send(packet, port, host);
|
|
1340
1383
|
retryCount++;
|
|
1341
1384
|
this.emit("debug", "discovery_send", { retryCount, host, port });
|
|
1342
|
-
} catch
|
|
1343
|
-
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
1385
|
+
} catch {
|
|
1344
1386
|
}
|
|
1345
1387
|
}
|
|
1346
1388
|
}
|
|
@@ -1349,6 +1391,7 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1349
1391
|
retryTimer = setIntervalNode(() => {
|
|
1350
1392
|
sendDiscovery();
|
|
1351
1393
|
}, retryInterval);
|
|
1394
|
+
this.discoveryTimers.push(retryTimer);
|
|
1352
1395
|
});
|
|
1353
1396
|
this.clientId = reply.cid;
|
|
1354
1397
|
this.cameraId = reply.did;
|
|
@@ -1665,6 +1708,10 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1665
1708
|
this.ackTimer = void 0;
|
|
1666
1709
|
this.resendTimer = void 0;
|
|
1667
1710
|
this.hbTimer = void 0;
|
|
1711
|
+
for (const t of this.discoveryTimers) {
|
|
1712
|
+
clearInterval(t);
|
|
1713
|
+
}
|
|
1714
|
+
this.discoveryTimers = [];
|
|
1668
1715
|
const s = this.sock;
|
|
1669
1716
|
this.sock = void 0;
|
|
1670
1717
|
if (!s) return;
|
|
@@ -1806,6 +1853,14 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1806
1853
|
* even if the current client instance is idle/disconnected.
|
|
1807
1854
|
*/
|
|
1808
1855
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
1856
|
+
/**
|
|
1857
|
+
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
1858
|
+
*
|
|
1859
|
+
* Why: when a D2C_DISC kills a client, the socket pool destroys the old instance
|
|
1860
|
+
* and creates a new one. Instance-level backoff variables would reset to zero,
|
|
1861
|
+
* allowing immediate reconnection and perpetuating the storm.
|
|
1862
|
+
*/
|
|
1863
|
+
static d2cDiscBackoff = /* @__PURE__ */ new Map();
|
|
1809
1864
|
/**
|
|
1810
1865
|
* Global (process-wide) CoverPreview serialization.
|
|
1811
1866
|
*
|
|
@@ -2501,7 +2556,12 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2501
2556
|
}
|
|
2502
2557
|
async waitForUdpReconnectCooldown() {
|
|
2503
2558
|
const now = Date.now();
|
|
2504
|
-
const
|
|
2559
|
+
const staticEntry = _BaichuanClient.d2cDiscBackoff.get(this.opts.host);
|
|
2560
|
+
const effectiveCooldownUntil = Math.max(
|
|
2561
|
+
this.udpReconnectCooldownUntilMs,
|
|
2562
|
+
staticEntry?.cooldownUntilMs ?? 0
|
|
2563
|
+
);
|
|
2564
|
+
const waitMs = effectiveCooldownUntil - now;
|
|
2505
2565
|
if (waitMs <= 0) return;
|
|
2506
2566
|
const sid = this.socketSessionId;
|
|
2507
2567
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -2510,7 +2570,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2510
2570
|
host: this.opts.host,
|
|
2511
2571
|
sid,
|
|
2512
2572
|
uid: shortUid,
|
|
2513
|
-
waitMs
|
|
2573
|
+
waitMs,
|
|
2574
|
+
persistent: staticEntry != null
|
|
2514
2575
|
});
|
|
2515
2576
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
2516
2577
|
}
|
|
@@ -2753,21 +2814,30 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2753
2814
|
uid: shortUid2,
|
|
2754
2815
|
message: err.message
|
|
2755
2816
|
});
|
|
2756
|
-
const
|
|
2817
|
+
const hostKey = this.opts.host;
|
|
2818
|
+
const prev = _BaichuanClient.d2cDiscBackoff.get(hostKey);
|
|
2819
|
+
const withinWindow = prev != null && now - prev.lastAtMs < 6e4;
|
|
2757
2820
|
const baseMs = 2e3;
|
|
2758
2821
|
const maxMs = 3e4;
|
|
2759
2822
|
const nextBackoffMs = withinWindow ? Math.min(
|
|
2760
2823
|
maxMs,
|
|
2761
2824
|
Math.max(
|
|
2762
2825
|
baseMs,
|
|
2763
|
-
|
|
2826
|
+
prev.backoffMs > 0 ? prev.backoffMs * 2 : baseMs
|
|
2764
2827
|
)
|
|
2765
2828
|
) : baseMs;
|
|
2766
|
-
|
|
2767
|
-
|
|
2829
|
+
const cooldownUntilMs = Math.max(
|
|
2830
|
+
prev?.cooldownUntilMs ?? 0,
|
|
2831
|
+
now + nextBackoffMs
|
|
2832
|
+
);
|
|
2833
|
+
_BaichuanClient.d2cDiscBackoff.set(hostKey, {
|
|
2834
|
+
backoffMs: nextBackoffMs,
|
|
2835
|
+
lastAtMs: now,
|
|
2836
|
+
cooldownUntilMs
|
|
2837
|
+
});
|
|
2768
2838
|
this.udpReconnectCooldownUntilMs = Math.max(
|
|
2769
2839
|
this.udpReconnectCooldownUntilMs,
|
|
2770
|
-
|
|
2840
|
+
cooldownUntilMs
|
|
2771
2841
|
);
|
|
2772
2842
|
this.logDebug("d2c_disc_backoff", {
|
|
2773
2843
|
transport: "udp",
|
|
@@ -2775,7 +2845,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2775
2845
|
sid: sid2,
|
|
2776
2846
|
uid: shortUid2,
|
|
2777
2847
|
backoffMs: nextBackoffMs,
|
|
2778
|
-
cooldownUntilMs
|
|
2848
|
+
cooldownUntilMs,
|
|
2849
|
+
persistent: true
|
|
2779
2850
|
});
|
|
2780
2851
|
this.stopKeepAlive();
|
|
2781
2852
|
this.loggedIn = false;
|
|
@@ -2784,6 +2855,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2784
2855
|
this.videoSubscriptions.clear();
|
|
2785
2856
|
this.recomputeGlobalStreamingContribution();
|
|
2786
2857
|
}
|
|
2858
|
+
this.emit("d2c_disc", { host: this.opts.host, atMs: now });
|
|
2787
2859
|
}
|
|
2788
2860
|
this.emit("error", err);
|
|
2789
2861
|
});
|
|
@@ -3082,6 +3154,13 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
3082
3154
|
}
|
|
3083
3155
|
}
|
|
3084
3156
|
this.emit("push", frame);
|
|
3157
|
+
if (frame.header.cmdId === 252 && frame.body.length > 0) {
|
|
3158
|
+
try {
|
|
3159
|
+
this.emit("batteryPush", frame);
|
|
3160
|
+
} catch (error) {
|
|
3161
|
+
this.logDebug("battery_push_error", error);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3085
3164
|
if (frame.header.cmdId === 33) {
|
|
3086
3165
|
try {
|
|
3087
3166
|
const sid = this.socketSessionId;
|
|
@@ -10149,6 +10228,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10149
10228
|
}
|
|
10150
10229
|
}
|
|
10151
10230
|
const newClient = new BaichuanClient(this.clientOptions);
|
|
10231
|
+
this.attachD2cDiscListener(newClient);
|
|
10152
10232
|
this.socketPool.set("general", {
|
|
10153
10233
|
client: newClient,
|
|
10154
10234
|
refCount: 1,
|
|
@@ -10194,6 +10274,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10194
10274
|
if (!mapped) return;
|
|
10195
10275
|
this.dispatchSimpleEvent(mapped);
|
|
10196
10276
|
});
|
|
10277
|
+
client.on("batteryPush", (frame) => {
|
|
10278
|
+
try {
|
|
10279
|
+
const xml = this.client.tryDecryptXml(
|
|
10280
|
+
frame.body,
|
|
10281
|
+
frame.header.channelId,
|
|
10282
|
+
this.client.enc
|
|
10283
|
+
);
|
|
10284
|
+
if (!xml) return;
|
|
10285
|
+
const channel = frame.header.channelId;
|
|
10286
|
+
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
10287
|
+
if (battery.batteryPercent !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
|
|
10288
|
+
this.dispatchSimpleEvent({
|
|
10289
|
+
type: "battery",
|
|
10290
|
+
channel,
|
|
10291
|
+
timestamp: Date.now(),
|
|
10292
|
+
battery
|
|
10293
|
+
});
|
|
10294
|
+
}
|
|
10295
|
+
} catch (e) {
|
|
10296
|
+
this.logger.debug?.(
|
|
10297
|
+
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
10298
|
+
formatErrorForLog(e)
|
|
10299
|
+
);
|
|
10300
|
+
}
|
|
10301
|
+
});
|
|
10197
10302
|
client.on("channelInfo", (xml) => {
|
|
10198
10303
|
try {
|
|
10199
10304
|
this.parseAndStoreChannelInfo(xml);
|
|
@@ -10329,6 +10434,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10329
10434
|
rtspServers = /* @__PURE__ */ new Set();
|
|
10330
10435
|
// Track all RTSP servers for cleanup
|
|
10331
10436
|
activeVideoMsgNums = /* @__PURE__ */ new Map();
|
|
10437
|
+
// ─── D2C_DISC cooldown & storm detection ────────────────────────────────────
|
|
10438
|
+
// Tracked on the API instance (survives BaichuanClient recreation).
|
|
10439
|
+
/** Timestamp of the most recent D2C_DISC from any client for this device. */
|
|
10440
|
+
lastD2cDiscAtMs = 0;
|
|
10441
|
+
/** Sliding window of recent D2C_DISC timestamps for storm detection. */
|
|
10442
|
+
d2cDiscTimestamps = [];
|
|
10443
|
+
/** Immediate cooldown (ms) applied to socket pool on every D2C_DISC.
|
|
10444
|
+
* Prevents reconnect attempts while the camera is transitioning to sleep. */
|
|
10445
|
+
static D2C_DISC_IMMEDIATE_COOLDOWN_MS = 1e4;
|
|
10446
|
+
/** Number of D2C_DISCs within the storm window to trigger extended cooldown. */
|
|
10447
|
+
static D2C_DISC_STORM_THRESHOLD = 3;
|
|
10448
|
+
/** Sliding window size (ms) for storm detection. */
|
|
10449
|
+
static D2C_DISC_STORM_WINDOW_MS = 6e4;
|
|
10450
|
+
/** Extended cooldown (ms) applied to socket pool when a D2C_DISC storm is detected. */
|
|
10451
|
+
static D2C_DISC_STORM_COOLDOWN_MS = 12e4;
|
|
10332
10452
|
nvrChannelsSummaryCache = /* @__PURE__ */ new Map();
|
|
10333
10453
|
/**
|
|
10334
10454
|
* Cached device capabilities per channel.
|
|
@@ -10653,6 +10773,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10653
10773
|
const prefix = basename.substring(0, 10).toUpperCase();
|
|
10654
10774
|
return prefix.includes("S") ? "subStream" : "mainStream";
|
|
10655
10775
|
}
|
|
10776
|
+
/**
|
|
10777
|
+
* Stream profiles that the device explicitly rejected (response_code 400).
|
|
10778
|
+
* Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
|
|
10779
|
+
* it is excluded from `buildVideoStreamOptions()` results and no further
|
|
10780
|
+
* start attempts are made until the API instance is recreated.
|
|
10781
|
+
*/
|
|
10782
|
+
_rejectedStreamProfiles = /* @__PURE__ */ new Set();
|
|
10783
|
+
/**
|
|
10784
|
+
* Check whether a stream profile was rejected by the device at runtime
|
|
10785
|
+
* (e.g. ext returned response_code 400).
|
|
10786
|
+
*/
|
|
10787
|
+
isStreamProfileRejected(channel, profile) {
|
|
10788
|
+
return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
|
|
10789
|
+
}
|
|
10656
10790
|
/**
|
|
10657
10791
|
* Cache for buildVideoStreamOptions.
|
|
10658
10792
|
*
|
|
@@ -10742,6 +10876,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10742
10876
|
}
|
|
10743
10877
|
return "general";
|
|
10744
10878
|
}
|
|
10879
|
+
/**
|
|
10880
|
+
* Attach a D2C_DISC listener to a BaichuanClient so that the API-level
|
|
10881
|
+
* grace period and storm detection are updated regardless of which
|
|
10882
|
+
* pool socket receives the disconnect.
|
|
10883
|
+
*/
|
|
10884
|
+
attachD2cDiscListener(client) {
|
|
10885
|
+
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
10886
|
+
}
|
|
10745
10887
|
/**
|
|
10746
10888
|
* Acquire a socket from the pool by tag.
|
|
10747
10889
|
* Creates a new socket if needed, or reuses an existing one.
|
|
@@ -10762,10 +10904,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10762
10904
|
);
|
|
10763
10905
|
} else if (now < cooldownEntry.cooldownUntil) {
|
|
10764
10906
|
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
10907
|
+
const isD2cDisc = this.lastD2cDiscAtMs > 0 && now - this.lastD2cDiscAtMs < 12e4;
|
|
10908
|
+
const reason = isD2cDisc ? "D2C_DISC (camera sleeping)" : "repeated login failures";
|
|
10765
10909
|
const error = new Error(
|
|
10766
|
-
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to
|
|
10910
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to ${reason}. tag=${tag}`
|
|
10767
10911
|
);
|
|
10768
|
-
log?.
|
|
10912
|
+
log?.debug?.(error.message);
|
|
10769
10913
|
throw error;
|
|
10770
10914
|
}
|
|
10771
10915
|
}
|
|
@@ -10856,12 +11000,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10856
11000
|
try {
|
|
10857
11001
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
10858
11002
|
const newClient = new BaichuanClient(clientOpts);
|
|
11003
|
+
this.attachD2cDiscListener(newClient);
|
|
10859
11004
|
await newClient.login();
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
)
|
|
10864
|
-
|
|
11005
|
+
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
11006
|
+
if (existingCooldown) {
|
|
11007
|
+
const isStormCooldown = existingCooldown.failureCount >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD;
|
|
11008
|
+
if (!isStormCooldown) {
|
|
11009
|
+
log?.debug?.(
|
|
11010
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
11011
|
+
);
|
|
11012
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
11013
|
+
} else {
|
|
11014
|
+
log?.debug?.(
|
|
11015
|
+
`[SocketPool] Preserving D2C_DISC storm cooldown for host=${this.host} (expires in ${Math.ceil((existingCooldown.cooldownUntil - Date.now()) / 1e3)}s)`
|
|
11016
|
+
);
|
|
11017
|
+
}
|
|
10865
11018
|
}
|
|
10866
11019
|
entry.client = newClient;
|
|
10867
11020
|
entry.refCount = 1;
|
|
@@ -11163,6 +11316,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11163
11316
|
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
11164
11317
|
};
|
|
11165
11318
|
const generalClient = new BaichuanClient(opts);
|
|
11319
|
+
this.attachD2cDiscListener(generalClient);
|
|
11166
11320
|
this.socketPool.set("general", {
|
|
11167
11321
|
client: generalClient,
|
|
11168
11322
|
refCount: 1,
|
|
@@ -15757,6 +15911,16 @@ ${stderr}`)
|
|
|
15757
15911
|
}
|
|
15758
15912
|
if (!frame) frame = await targetClient.sendFrame(baseParams);
|
|
15759
15913
|
if (frame.header.responseCode !== 200) {
|
|
15914
|
+
if (frame.header.responseCode === 400) {
|
|
15915
|
+
const rejKey = `${ch}:${profile}`;
|
|
15916
|
+
if (!this._rejectedStreamProfiles.has(rejKey)) {
|
|
15917
|
+
this._rejectedStreamProfiles.add(rejKey);
|
|
15918
|
+
this.videoStreamOptionsCache.clear();
|
|
15919
|
+
this.logger?.warn?.(
|
|
15920
|
+
`[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.`
|
|
15921
|
+
);
|
|
15922
|
+
}
|
|
15923
|
+
}
|
|
15760
15924
|
throw new Error(
|
|
15761
15925
|
`Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
|
|
15762
15926
|
);
|
|
@@ -16227,6 +16391,49 @@ ${stderr}`)
|
|
|
16227
16391
|
if (batteryVersion !== void 0) out.batteryVersion = batteryVersion;
|
|
16228
16392
|
return out;
|
|
16229
16393
|
}
|
|
16394
|
+
/**
|
|
16395
|
+
* Called when any BaichuanClient for this device receives a D2C_DISC.
|
|
16396
|
+
*
|
|
16397
|
+
* Two-tier response:
|
|
16398
|
+
* 1. **Immediate**: every D2C_DISC applies a short socket pool cooldown
|
|
16399
|
+
* (10 s) to prevent reconnect attempts while the camera transitions to sleep.
|
|
16400
|
+
* 2. **Storm**: ≥3 D2C_DISCs within 60 s triggers extended cooldown (120 s).
|
|
16401
|
+
*/
|
|
16402
|
+
notifyD2cDisc() {
|
|
16403
|
+
const now = Date.now();
|
|
16404
|
+
this.lastD2cDiscAtMs = now;
|
|
16405
|
+
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
16406
|
+
const existing = this.socketPoolCooldowns.get(this.host);
|
|
16407
|
+
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
16408
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
16409
|
+
failureCount: existing?.failureCount ?? 1,
|
|
16410
|
+
lastFailureAt: now,
|
|
16411
|
+
cooldownUntil: immediateCooldownUntil
|
|
16412
|
+
});
|
|
16413
|
+
this.logger?.log?.(
|
|
16414
|
+
`[D2C_DISC] Immediate cooldown: socket pool blocked for ${_ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS / 1e3}s`
|
|
16415
|
+
);
|
|
16416
|
+
}
|
|
16417
|
+
this.d2cDiscTimestamps.push(now);
|
|
16418
|
+
const cutoff = now - _ReolinkBaichuanApi.D2C_DISC_STORM_WINDOW_MS;
|
|
16419
|
+
while (this.d2cDiscTimestamps.length > 0 && this.d2cDiscTimestamps[0] < cutoff) {
|
|
16420
|
+
this.d2cDiscTimestamps.shift();
|
|
16421
|
+
}
|
|
16422
|
+
if (this.d2cDiscTimestamps.length >= _ReolinkBaichuanApi.D2C_DISC_STORM_THRESHOLD) {
|
|
16423
|
+
const stormCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_STORM_COOLDOWN_MS;
|
|
16424
|
+
const currentEntry = this.socketPoolCooldowns.get(this.host);
|
|
16425
|
+
if (!currentEntry || currentEntry.cooldownUntil < stormCooldownUntil) {
|
|
16426
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
16427
|
+
failureCount: this.d2cDiscTimestamps.length,
|
|
16428
|
+
lastFailureAt: now,
|
|
16429
|
+
cooldownUntil: stormCooldownUntil
|
|
16430
|
+
});
|
|
16431
|
+
this.logger?.warn?.(
|
|
16432
|
+
`[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`
|
|
16433
|
+
);
|
|
16434
|
+
}
|
|
16435
|
+
}
|
|
16436
|
+
}
|
|
16230
16437
|
/**
|
|
16231
16438
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
16232
16439
|
*
|
|
@@ -16257,6 +16464,8 @@ ${stderr}`)
|
|
|
16257
16464
|
const socketConnected = this.client.isSocketConnected?.() ?? false;
|
|
16258
16465
|
const now = Date.now();
|
|
16259
16466
|
const cutoff = now - windowMs;
|
|
16467
|
+
const msSinceD2cDisc = now - this.lastD2cDiscAtMs;
|
|
16468
|
+
const recentD2cDisc = this.lastD2cDiscAtMs > 0 && msSinceD2cDisc < 3e4;
|
|
16260
16469
|
const rx = (this.client.getRxHistory?.() ?? []).filter(
|
|
16261
16470
|
(h) => h.atMs >= cutoff
|
|
16262
16471
|
);
|
|
@@ -16264,6 +16473,12 @@ ${stderr}`)
|
|
|
16264
16473
|
(h) => h.atMs >= cutoff
|
|
16265
16474
|
);
|
|
16266
16475
|
if (rx.length === 0 && tx.length === 0) {
|
|
16476
|
+
if (recentD2cDisc) {
|
|
16477
|
+
return {
|
|
16478
|
+
state: "sleeping",
|
|
16479
|
+
reason: `D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
16480
|
+
};
|
|
16481
|
+
}
|
|
16267
16482
|
return {
|
|
16268
16483
|
state: "sleeping",
|
|
16269
16484
|
reason: `no rx/tx activity in last ${windowMs}ms${socketConnected ? "" : " (socket disconnected)"}`,
|
|
@@ -16287,6 +16502,12 @@ ${stderr}`)
|
|
|
16287
16502
|
idleMs: now - firstWakingTx.atMs
|
|
16288
16503
|
};
|
|
16289
16504
|
}
|
|
16505
|
+
if (recentD2cDisc) {
|
|
16506
|
+
return {
|
|
16507
|
+
state: "sleeping",
|
|
16508
|
+
reason: `only non-waking cmdIds + D2C_DISC ${Math.round(msSinceD2cDisc / 1e3)}s ago, camera terminated session`
|
|
16509
|
+
};
|
|
16510
|
+
}
|
|
16290
16511
|
return {
|
|
16291
16512
|
state: "sleeping",
|
|
16292
16513
|
reason: `only non-waking cmdIds observed in last ${windowMs}ms (non-waking: ${Array.from(nonWakingCmdIds).join(",")})`,
|
|
@@ -17721,6 +17942,8 @@ ${xml}`
|
|
|
17721
17942
|
for (const metadata of params.metadatas) {
|
|
17722
17943
|
const profile = metadata.profile;
|
|
17723
17944
|
if (isMultiFocal && profile === "ext") continue;
|
|
17945
|
+
if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
|
|
17946
|
+
continue;
|
|
17724
17947
|
if (params.includeRtsp && profile !== "ext") {
|
|
17725
17948
|
const streamName = profile === "main" ? "main" : "sub";
|
|
17726
17949
|
pushRtsp({
|
|
@@ -17882,7 +18105,7 @@ ${xml}`
|
|
|
17882
18105
|
* @returns Test results for all stream types and profiles
|
|
17883
18106
|
*/
|
|
17884
18107
|
async testChannelStreams(channel, logger) {
|
|
17885
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
18108
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-UMN4C7SY.js");
|
|
17886
18109
|
return await testChannelStreams({
|
|
17887
18110
|
api: this,
|
|
17888
18111
|
channel: this.normalizeChannel(channel),
|
|
@@ -17898,7 +18121,7 @@ ${xml}`
|
|
|
17898
18121
|
* @returns Complete diagnostics for all channels and streams
|
|
17899
18122
|
*/
|
|
17900
18123
|
async collectMultifocalDiagnostics(logger) {
|
|
17901
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
18124
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-UMN4C7SY.js");
|
|
17902
18125
|
return await collectMultifocalDiagnostics({
|
|
17903
18126
|
api: this,
|
|
17904
18127
|
logger
|
|
@@ -21580,4 +21803,4 @@ export {
|
|
|
21580
21803
|
isTcpFailureThatShouldFallbackToUdp,
|
|
21581
21804
|
autoDetectDeviceType
|
|
21582
21805
|
};
|
|
21583
|
-
//# sourceMappingURL=chunk-
|
|
21806
|
+
//# sourceMappingURL=chunk-F2Y5U3YP.js.map
|