@apocaliss92/nodelink-js 0.1.9 → 0.1.17
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 +257 -9
- package/dist/{DiagnosticsTools-EC7DADEQ.js → DiagnosticsTools-6WEMO4L4.js} +2 -2
- package/dist/{chunk-YUBYINJF.js → chunk-ULSFEQSE.js} +1764 -375
- package/dist/chunk-ULSFEQSE.js.map +1 -0
- package/dist/{chunk-TZFZ5WJX.js → chunk-ZE7D7LI4.js} +40 -9
- package/dist/chunk-ZE7D7LI4.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +1781 -367
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +1940 -369
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +312 -50
- package/dist/index.d.ts +314 -49
- package/dist/index.js +158 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-TZFZ5WJX.js.map +0 -1
- package/dist/chunk-YUBYINJF.js.map +0 -1
- /package/dist/{DiagnosticsTools-EC7DADEQ.js.map → DiagnosticsTools-6WEMO4L4.js.map} +0 -0
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
BC_CMD_ID_GET_WIFI,
|
|
59
59
|
BC_CMD_ID_GET_WIFI_SIGNAL,
|
|
60
60
|
BC_CMD_ID_GET_ZOOM_FOCUS,
|
|
61
|
+
BC_CMD_ID_LOGOUT,
|
|
61
62
|
BC_CMD_ID_PING,
|
|
62
63
|
BC_CMD_ID_PTZ_CONTROL,
|
|
63
64
|
BC_CMD_ID_PTZ_CONTROL_PRESET,
|
|
@@ -101,6 +102,7 @@ import {
|
|
|
101
102
|
buildChannelExtensionXml,
|
|
102
103
|
buildFloodlightManualXml,
|
|
103
104
|
buildLoginXml,
|
|
105
|
+
buildLogoutXml,
|
|
104
106
|
buildPreviewStopXml,
|
|
105
107
|
buildPreviewStopXmlV11,
|
|
106
108
|
buildPreviewXml,
|
|
@@ -134,7 +136,7 @@ import {
|
|
|
134
136
|
talkTraceLog,
|
|
135
137
|
traceLog,
|
|
136
138
|
xmlEscape
|
|
137
|
-
} from "./chunk-
|
|
139
|
+
} from "./chunk-ZE7D7LI4.js";
|
|
138
140
|
|
|
139
141
|
// src/protocol/framing.ts
|
|
140
142
|
function encodeHeader(h) {
|
|
@@ -692,7 +694,9 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
692
694
|
});
|
|
693
695
|
sock.on("error", (e) => this.emit("error", e));
|
|
694
696
|
sock.on("close", () => this.emit("close"));
|
|
695
|
-
await new Promise(
|
|
697
|
+
await new Promise(
|
|
698
|
+
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
699
|
+
);
|
|
696
700
|
if (this.opts.mode === "direct") {
|
|
697
701
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
698
702
|
this.clientId = this.opts.clientId;
|
|
@@ -703,7 +707,8 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
703
707
|
this.startTimers();
|
|
704
708
|
}
|
|
705
709
|
async discoveryUid(sock) {
|
|
706
|
-
if (this.opts.mode !== "uid")
|
|
710
|
+
if (this.opts.mode !== "uid")
|
|
711
|
+
throw new Error("Internal: discoveryUid called for non-uid mode");
|
|
707
712
|
const method = this.opts.discoveryMethod ?? "local-direct";
|
|
708
713
|
if (method === "local-broadcast" || method === "local-direct") {
|
|
709
714
|
await this.discoveryUidLocal(sock, { localMode: method });
|
|
@@ -725,10 +730,13 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
725
730
|
}
|
|
726
731
|
}
|
|
727
732
|
}
|
|
728
|
-
throw new Error(
|
|
733
|
+
throw new Error(
|
|
734
|
+
"No non-loopback IPv4 address found (required for P2P discovery)"
|
|
735
|
+
);
|
|
729
736
|
}
|
|
730
737
|
async discoveryUidP2p(sock, method) {
|
|
731
|
-
if (this.opts.mode !== "uid")
|
|
738
|
+
if (this.opts.mode !== "uid")
|
|
739
|
+
throw new Error("Internal: discoveryUidP2p called for non-uid mode");
|
|
732
740
|
const addr = sock.address();
|
|
733
741
|
const localPort = typeof addr === "string" ? 0 : addr.port;
|
|
734
742
|
const cid = Math.floor(Math.random() * 2147483647) | 0 || 82e3;
|
|
@@ -741,7 +749,11 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
741
749
|
reg: lookup.reg
|
|
742
750
|
});
|
|
743
751
|
const conn = method === "remote" ? "local" : method;
|
|
744
|
-
const connected = await this.p2pConnect(
|
|
752
|
+
const connected = await this.p2pConnect(
|
|
753
|
+
sock,
|
|
754
|
+
{ ...reg, uid: this.opts.uid, cid },
|
|
755
|
+
method
|
|
756
|
+
);
|
|
745
757
|
await this.p2pConfirm(sock, reg.reg, {
|
|
746
758
|
sid: reg.sid,
|
|
747
759
|
conn,
|
|
@@ -760,13 +772,16 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
760
772
|
try {
|
|
761
773
|
const answers = await dns.lookup(host, { family: 4, all: true });
|
|
762
774
|
for (const a of answers) {
|
|
763
|
-
if (a.address && !resolved.includes(a.address))
|
|
775
|
+
if (a.address && !resolved.includes(a.address))
|
|
776
|
+
resolved.push(a.address);
|
|
764
777
|
}
|
|
765
778
|
} catch {
|
|
766
779
|
}
|
|
767
780
|
}
|
|
768
781
|
if (resolved.length === 0) {
|
|
769
|
-
throw new Error(
|
|
782
|
+
throw new Error(
|
|
783
|
+
"P2P UID lookup failed: no p2p.reolink.com addresses resolved"
|
|
784
|
+
);
|
|
770
785
|
}
|
|
771
786
|
const start = Date.now();
|
|
772
787
|
let lastErr;
|
|
@@ -774,7 +789,12 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
774
789
|
const remaining = P2P_MAX_WAIT_MS - (Date.now() - start);
|
|
775
790
|
if (remaining <= 0) break;
|
|
776
791
|
try {
|
|
777
|
-
const res = await this.p2pUidLookupOne(
|
|
792
|
+
const res = await this.p2pUidLookupOne(
|
|
793
|
+
sock,
|
|
794
|
+
uid,
|
|
795
|
+
{ host: ip, port: P2P_LOOKUP_PORT },
|
|
796
|
+
Math.min(remaining, 3e3)
|
|
797
|
+
);
|
|
778
798
|
return res;
|
|
779
799
|
} catch (e) {
|
|
780
800
|
lastErr = e instanceof Error ? e : new Error(String(e));
|
|
@@ -896,26 +916,64 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
896
916
|
async p2pConnect(sock, reg, method) {
|
|
897
917
|
if (method === "remote") {
|
|
898
918
|
const tasks = [];
|
|
899
|
-
if (reg.dev)
|
|
900
|
-
|
|
901
|
-
|
|
919
|
+
if (reg.dev)
|
|
920
|
+
tasks.push(
|
|
921
|
+
this.p2pClientInitiated(sock, {
|
|
922
|
+
sid: reg.sid,
|
|
923
|
+
cid: reg.cid,
|
|
924
|
+
conn: "local",
|
|
925
|
+
dest: reg.dev
|
|
926
|
+
})
|
|
927
|
+
);
|
|
928
|
+
if (reg.dmap)
|
|
929
|
+
tasks.push(
|
|
930
|
+
this.p2pDeviceInitiated(sock, {
|
|
931
|
+
sid: reg.sid,
|
|
932
|
+
cid: reg.cid,
|
|
933
|
+
conn: "local",
|
|
934
|
+
expectFrom: reg.dmap
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
if (tasks.length === 0)
|
|
938
|
+
throw new Error("P2P remote discovery: missing dev/dmap addresses");
|
|
902
939
|
return await Promise.race(tasks);
|
|
903
940
|
}
|
|
904
941
|
if (method === "map") {
|
|
905
942
|
if (!reg.dmap) throw new Error("P2P map discovery: missing dmap address");
|
|
906
|
-
return await this.p2pDeviceInitiated(sock, {
|
|
943
|
+
return await this.p2pDeviceInitiated(sock, {
|
|
944
|
+
sid: reg.sid,
|
|
945
|
+
cid: reg.cid,
|
|
946
|
+
conn: "map",
|
|
947
|
+
expectFrom: reg.dmap
|
|
948
|
+
});
|
|
907
949
|
}
|
|
908
|
-
if (!reg.relay)
|
|
909
|
-
|
|
950
|
+
if (!reg.relay)
|
|
951
|
+
throw new Error("P2P relay discovery: missing relay address");
|
|
952
|
+
return await this.p2pClientInitiated(sock, {
|
|
953
|
+
sid: reg.sid,
|
|
954
|
+
cid: reg.cid,
|
|
955
|
+
conn: "relay",
|
|
956
|
+
dest: reg.relay,
|
|
957
|
+
requireConnMatch: true
|
|
958
|
+
});
|
|
910
959
|
}
|
|
911
960
|
async p2pClientInitiated(sock, params) {
|
|
912
961
|
const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
|
|
913
|
-
const xml = buildC2dT({
|
|
962
|
+
const xml = buildC2dT({
|
|
963
|
+
sid: params.sid,
|
|
964
|
+
conn: params.conn,
|
|
965
|
+
cid: params.cid,
|
|
966
|
+
mtu: this.mtu
|
|
967
|
+
});
|
|
914
968
|
const pkt = encodeDiscoveryPacket(tid, xml);
|
|
915
969
|
return await new Promise((resolve, reject) => {
|
|
916
970
|
const timeout = setTimeout(() => {
|
|
917
971
|
cleanup();
|
|
918
|
-
reject(
|
|
972
|
+
reject(
|
|
973
|
+
new Error(
|
|
974
|
+
`P2P client-initiated (${params.conn}) timeout after ${P2P_MAX_WAIT_MS}ms`
|
|
975
|
+
)
|
|
976
|
+
);
|
|
919
977
|
}, P2P_MAX_WAIT_MS);
|
|
920
978
|
const onMsg = (msg, rinfo) => {
|
|
921
979
|
try {
|
|
@@ -925,10 +983,16 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
925
983
|
if (!cfm) return;
|
|
926
984
|
if (cfm.cid !== params.cid) return;
|
|
927
985
|
if (cfm.sid !== params.sid) return;
|
|
928
|
-
if (params.requireConnMatch && (cfm.conn ?? "") !== params.conn)
|
|
986
|
+
if (params.requireConnMatch && (cfm.conn ?? "") !== params.conn)
|
|
987
|
+
return;
|
|
929
988
|
if (cfm.did == null) return;
|
|
930
989
|
cleanup();
|
|
931
|
-
resolve({
|
|
990
|
+
resolve({
|
|
991
|
+
did: cfm.did,
|
|
992
|
+
rhost: rinfo.address,
|
|
993
|
+
rport: rinfo.port,
|
|
994
|
+
tid
|
|
995
|
+
});
|
|
932
996
|
} catch {
|
|
933
997
|
}
|
|
934
998
|
};
|
|
@@ -952,7 +1016,11 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
952
1016
|
return await new Promise((resolve, reject) => {
|
|
953
1017
|
const timeout = setTimeout(() => {
|
|
954
1018
|
cleanup();
|
|
955
|
-
reject(
|
|
1019
|
+
reject(
|
|
1020
|
+
new Error(
|
|
1021
|
+
`P2P device-initiated (${params.conn}) timeout after ${P2P_MAX_WAIT_MS}ms`
|
|
1022
|
+
)
|
|
1023
|
+
);
|
|
956
1024
|
}, P2P_MAX_WAIT_MS);
|
|
957
1025
|
let state = "wait_t";
|
|
958
1026
|
let did;
|
|
@@ -961,9 +1029,16 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
961
1029
|
let rport;
|
|
962
1030
|
let resendA;
|
|
963
1031
|
const sendA = () => {
|
|
964
|
-
if (state !== "wait_cfm" || did == null || tid == null || rhost == null || rport == null)
|
|
1032
|
+
if (state !== "wait_cfm" || did == null || tid == null || rhost == null || rport == null)
|
|
1033
|
+
return;
|
|
965
1034
|
try {
|
|
966
|
-
const aXml = buildC2dA({
|
|
1035
|
+
const aXml = buildC2dA({
|
|
1036
|
+
sid: params.sid,
|
|
1037
|
+
conn: params.conn,
|
|
1038
|
+
cid: params.cid,
|
|
1039
|
+
did,
|
|
1040
|
+
mtu: this.mtu
|
|
1041
|
+
});
|
|
967
1042
|
const aPkt = encodeDiscoveryPacket(tid, aXml);
|
|
968
1043
|
sock.send(aPkt, rport, rhost);
|
|
969
1044
|
} catch {
|
|
@@ -999,7 +1074,12 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
999
1074
|
if (cfm.did !== did) return;
|
|
1000
1075
|
if ((cfm.conn ?? "") !== params.conn) return;
|
|
1001
1076
|
cleanup();
|
|
1002
|
-
resolve({
|
|
1077
|
+
resolve({
|
|
1078
|
+
did,
|
|
1079
|
+
rhost: info.address,
|
|
1080
|
+
rport: info.port,
|
|
1081
|
+
tid: tid ?? 0
|
|
1082
|
+
});
|
|
1003
1083
|
} catch {
|
|
1004
1084
|
}
|
|
1005
1085
|
};
|
|
@@ -1012,7 +1092,13 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1012
1092
|
});
|
|
1013
1093
|
}
|
|
1014
1094
|
async p2pConfirm(sock, reg, params) {
|
|
1015
|
-
const xml = buildC2rCfm({
|
|
1095
|
+
const xml = buildC2rCfm({
|
|
1096
|
+
sid: params.sid,
|
|
1097
|
+
conn: params.conn,
|
|
1098
|
+
rsp: 0,
|
|
1099
|
+
cid: params.cid,
|
|
1100
|
+
did: params.did
|
|
1101
|
+
});
|
|
1016
1102
|
for (let i = 0; i < 5; i++) {
|
|
1017
1103
|
const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
|
|
1018
1104
|
const pkt = encodeDiscoveryPacket(tid, xml);
|
|
@@ -1024,8 +1110,12 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1024
1110
|
}
|
|
1025
1111
|
}
|
|
1026
1112
|
async discoveryUidLocal(sock, opts) {
|
|
1027
|
-
if (this.opts.mode !== "uid")
|
|
1028
|
-
|
|
1113
|
+
if (this.opts.mode !== "uid")
|
|
1114
|
+
throw new Error("Internal: discoveryUidLocal called for non-uid mode");
|
|
1115
|
+
const ports = [
|
|
1116
|
+
BCUDP_DISCOVERY_PORT_LOCAL_ANY,
|
|
1117
|
+
BCUDP_DISCOVERY_PORT_LOCAL_UID
|
|
1118
|
+
];
|
|
1029
1119
|
const broadcastHost = "255.255.255.255";
|
|
1030
1120
|
const directHost = (this.opts.directHost ?? "").trim();
|
|
1031
1121
|
const localMode = opts?.localMode ?? "local-broadcast";
|
|
@@ -1037,12 +1127,21 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1037
1127
|
const addr = sock.address();
|
|
1038
1128
|
const localPort = typeof addr === "string" ? 0 : addr.port;
|
|
1039
1129
|
const cid = Math.floor(Math.random() * 2147483647) | 0 || 82e3;
|
|
1040
|
-
const xml = buildC2dC({
|
|
1130
|
+
const xml = buildC2dC({
|
|
1131
|
+
uid: this.opts.uid,
|
|
1132
|
+
clientPort: localPort,
|
|
1133
|
+
cid,
|
|
1134
|
+
mtu: this.mtu
|
|
1135
|
+
});
|
|
1041
1136
|
const reply = await new Promise((resolve, reject) => {
|
|
1042
1137
|
const timeout = setTimeout(() => {
|
|
1043
1138
|
if (retryTimer) clearInterval(retryTimer);
|
|
1044
1139
|
sock.off("message", onMsg);
|
|
1045
|
-
reject(
|
|
1140
|
+
reject(
|
|
1141
|
+
new Error(
|
|
1142
|
+
`BCUDP discovery timeout after ${discoveryTimeout}ms (camera may be sleeping or unreachable)`
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
1046
1145
|
}, discoveryTimeout);
|
|
1047
1146
|
let retryTimer;
|
|
1048
1147
|
let retryCount = 0;
|
|
@@ -1064,8 +1163,16 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1064
1163
|
sock.off("message", onMsg);
|
|
1065
1164
|
clearTimeout(timeout);
|
|
1066
1165
|
if (retryTimer) clearInterval(retryTimer);
|
|
1067
|
-
this.emit("debug", "discovery_finalize", {
|
|
1068
|
-
|
|
1166
|
+
this.emit("debug", "discovery_finalize", {
|
|
1167
|
+
reason,
|
|
1168
|
+
...sid != null ? { sid } : {},
|
|
1169
|
+
...d
|
|
1170
|
+
});
|
|
1171
|
+
resolve({
|
|
1172
|
+
...d,
|
|
1173
|
+
...sid != null ? { sid } : {},
|
|
1174
|
+
...tid != null ? { tid } : {}
|
|
1175
|
+
});
|
|
1069
1176
|
}, 250);
|
|
1070
1177
|
};
|
|
1071
1178
|
const sendT = (rhost, rport) => {
|
|
@@ -1074,11 +1181,22 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1074
1181
|
if (discoveredSid == null) return;
|
|
1075
1182
|
try {
|
|
1076
1183
|
const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
|
|
1077
|
-
const tXml = buildC2dT({
|
|
1184
|
+
const tXml = buildC2dT({
|
|
1185
|
+
...discoveredSid != null ? { sid: discoveredSid } : {},
|
|
1186
|
+
cid: discovered.cid,
|
|
1187
|
+
mtu: this.mtu,
|
|
1188
|
+
conn: "local"
|
|
1189
|
+
});
|
|
1078
1190
|
const tPkt = encodeDiscoveryPacket(tid, tXml);
|
|
1079
1191
|
sock.send(tPkt, rport, rhost);
|
|
1080
1192
|
sentT = true;
|
|
1081
|
-
this.emit("debug", "discovery_t_send", {
|
|
1193
|
+
this.emit("debug", "discovery_t_send", {
|
|
1194
|
+
sid: discoveredSid,
|
|
1195
|
+
cid: discovered.cid,
|
|
1196
|
+
did: discovered.did,
|
|
1197
|
+
rhost,
|
|
1198
|
+
rport
|
|
1199
|
+
});
|
|
1082
1200
|
} catch (e) {
|
|
1083
1201
|
this.emit("debug", "discovery_t_send_error", e);
|
|
1084
1202
|
}
|
|
@@ -1086,11 +1204,23 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1086
1204
|
const sendA = (tid, rhost, rport, dt) => {
|
|
1087
1205
|
if (sentA) return;
|
|
1088
1206
|
try {
|
|
1089
|
-
const aXml = buildC2dA({
|
|
1207
|
+
const aXml = buildC2dA({
|
|
1208
|
+
sid: dt.sid,
|
|
1209
|
+
conn: dt.conn ?? "local",
|
|
1210
|
+
cid: dt.cid,
|
|
1211
|
+
did: dt.did,
|
|
1212
|
+
mtu: this.mtu
|
|
1213
|
+
});
|
|
1090
1214
|
const aPkt = encodeDiscoveryPacket(tid, aXml);
|
|
1091
1215
|
sock.send(aPkt, rport, rhost);
|
|
1092
1216
|
sentA = true;
|
|
1093
|
-
this.emit("debug", "discovery_a_send", {
|
|
1217
|
+
this.emit("debug", "discovery_a_send", {
|
|
1218
|
+
sid: dt.sid,
|
|
1219
|
+
cid: dt.cid,
|
|
1220
|
+
did: dt.did,
|
|
1221
|
+
rhost,
|
|
1222
|
+
rport
|
|
1223
|
+
});
|
|
1094
1224
|
} catch (e) {
|
|
1095
1225
|
this.emit("debug", "discovery_a_send_error", e);
|
|
1096
1226
|
}
|
|
@@ -1099,12 +1229,22 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1099
1229
|
try {
|
|
1100
1230
|
const p = decodeBcUdpPacket(msg);
|
|
1101
1231
|
if (p.kind !== "discovery") return;
|
|
1102
|
-
this.emit("debug", "discovery_rx", {
|
|
1232
|
+
this.emit("debug", "discovery_rx", {
|
|
1233
|
+
tid: p.tid,
|
|
1234
|
+
rhost: rinfo.address,
|
|
1235
|
+
rport: rinfo.port,
|
|
1236
|
+
xmlPreview: p.xml.slice(0, 120)
|
|
1237
|
+
});
|
|
1103
1238
|
const cfm = parseD2cCfm(p.xml);
|
|
1104
1239
|
if (cfm) {
|
|
1105
1240
|
discoveredSid = cfm.sid;
|
|
1106
1241
|
if (!discovered && cfm.cid != null && cfm.did != null) {
|
|
1107
|
-
discovered = {
|
|
1242
|
+
discovered = {
|
|
1243
|
+
cid: cfm.cid,
|
|
1244
|
+
did: cfm.did,
|
|
1245
|
+
rhost: rinfo.address,
|
|
1246
|
+
rport: rinfo.port
|
|
1247
|
+
};
|
|
1108
1248
|
}
|
|
1109
1249
|
if (discovered) {
|
|
1110
1250
|
sendT(rinfo.address, rinfo.port);
|
|
@@ -1116,9 +1256,20 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1116
1256
|
gotT = true;
|
|
1117
1257
|
discoveredSid = dt.sid;
|
|
1118
1258
|
if (!discovered) {
|
|
1119
|
-
discovered = {
|
|
1259
|
+
discovered = {
|
|
1260
|
+
cid: dt.cid,
|
|
1261
|
+
did: dt.did,
|
|
1262
|
+
rhost: rinfo.address,
|
|
1263
|
+
rport: rinfo.port
|
|
1264
|
+
};
|
|
1120
1265
|
}
|
|
1121
|
-
this.emit("debug", "discovery_t_rx", {
|
|
1266
|
+
this.emit("debug", "discovery_t_rx", {
|
|
1267
|
+
sid: dt.sid,
|
|
1268
|
+
cid: dt.cid,
|
|
1269
|
+
did: dt.did,
|
|
1270
|
+
rhost: rinfo.address,
|
|
1271
|
+
rport: rinfo.port
|
|
1272
|
+
});
|
|
1122
1273
|
sendA(p.tid, rinfo.address, rinfo.port, dt);
|
|
1123
1274
|
maybeFinalize("t_a");
|
|
1124
1275
|
return;
|
|
@@ -1126,9 +1277,20 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1126
1277
|
const parsed = parseD2cCr(p.xml);
|
|
1127
1278
|
if (!parsed) return;
|
|
1128
1279
|
if (parsed.rsp !== 0) return;
|
|
1129
|
-
this.emit("debug", "discovery_success", {
|
|
1280
|
+
this.emit("debug", "discovery_success", {
|
|
1281
|
+
retryCount,
|
|
1282
|
+
rhost: rinfo.address,
|
|
1283
|
+
rport: rinfo.port,
|
|
1284
|
+
sid: parsed.sid ?? discoveredSid,
|
|
1285
|
+
timer: parsed.timer
|
|
1286
|
+
});
|
|
1130
1287
|
discoveredTid = p.tid;
|
|
1131
|
-
discovered = {
|
|
1288
|
+
discovered = {
|
|
1289
|
+
cid: parsed.cid,
|
|
1290
|
+
did: parsed.did,
|
|
1291
|
+
rhost: rinfo.address,
|
|
1292
|
+
rport: rinfo.port
|
|
1293
|
+
};
|
|
1132
1294
|
if (parsed.sid != null) discoveredSid = parsed.sid;
|
|
1133
1295
|
sendT(rinfo.address, rinfo.port);
|
|
1134
1296
|
if (gotT || sentA) {
|
|
@@ -1155,7 +1317,8 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1155
1317
|
const hosts = (() => {
|
|
1156
1318
|
if (localMode === "local-direct") {
|
|
1157
1319
|
if (directHost) {
|
|
1158
|
-
if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
|
|
1320
|
+
if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
|
|
1321
|
+
return [directHost];
|
|
1159
1322
|
return [directHost, broadcastHost];
|
|
1160
1323
|
}
|
|
1161
1324
|
return [broadcastHost];
|
|
@@ -1214,12 +1377,18 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1214
1377
|
}, 1e3);
|
|
1215
1378
|
}
|
|
1216
1379
|
sendHeartbeat() {
|
|
1217
|
-
if (!this.sock || !this.remote || this.clientId == null || this.cameraId == null)
|
|
1380
|
+
if (!this.sock || !this.remote || this.clientId == null || this.cameraId == null)
|
|
1381
|
+
return;
|
|
1218
1382
|
const tid = this.getKeepAliveTid();
|
|
1219
1383
|
let xml;
|
|
1220
1384
|
xml = buildC2dHb({ cid: this.clientId, did: this.cameraId });
|
|
1221
1385
|
const pkt = encodeDiscoveryPacket(tid, xml);
|
|
1222
|
-
this.emit("debug", "udp_hb_send", {
|
|
1386
|
+
this.emit("debug", "udp_hb_send", {
|
|
1387
|
+
tid,
|
|
1388
|
+
xml,
|
|
1389
|
+
host: this.remote.host,
|
|
1390
|
+
port: this.remote.port
|
|
1391
|
+
});
|
|
1223
1392
|
this.sock.send(pkt, this.remote.port, this.remote.host);
|
|
1224
1393
|
}
|
|
1225
1394
|
buildAckPayload() {
|
|
@@ -1263,7 +1432,9 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1263
1432
|
this.sock.send(this.lastAckPacket, this.remote.port, this.remote.host);
|
|
1264
1433
|
this.ackSentCount++;
|
|
1265
1434
|
if (this.ackSentCount % 100 === 0) {
|
|
1266
|
-
this.emit("debug", "ack_sent_100", {
|
|
1435
|
+
this.emit("debug", "ack_sent_100", {
|
|
1436
|
+
latency: this.ackLatency.getValue()
|
|
1437
|
+
});
|
|
1267
1438
|
}
|
|
1268
1439
|
}
|
|
1269
1440
|
scheduleAck(reason = "data") {
|
|
@@ -1338,7 +1509,11 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1338
1509
|
const updateRemote = (reason) => {
|
|
1339
1510
|
if (!this.remote || this.remote.host !== rhost || this.remote.port !== rport) {
|
|
1340
1511
|
this.remote = { host: rhost, port: rport };
|
|
1341
|
-
this.emit("debug", "remote_update", {
|
|
1512
|
+
this.emit("debug", "remote_update", {
|
|
1513
|
+
reason,
|
|
1514
|
+
host: rhost,
|
|
1515
|
+
port: rport
|
|
1516
|
+
});
|
|
1342
1517
|
}
|
|
1343
1518
|
};
|
|
1344
1519
|
if (!this.remote) updateRemote("first_packet");
|
|
@@ -1357,7 +1532,9 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1357
1532
|
this.flushReceived();
|
|
1358
1533
|
this.updateAckPacket();
|
|
1359
1534
|
if (this.packetsWant % 100 === 0) {
|
|
1360
|
-
this.emit("debug", "udp_progress", {
|
|
1535
|
+
this.emit("debug", "udp_progress", {
|
|
1536
|
+
packetsWant: this.packetsWant
|
|
1537
|
+
});
|
|
1361
1538
|
}
|
|
1362
1539
|
}
|
|
1363
1540
|
}
|
|
@@ -1367,7 +1544,11 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1367
1544
|
this.emit("debug", "discovery_rx_connected", { tid: p.tid, xml: p.xml });
|
|
1368
1545
|
const hb = parseD2cHb(p.xml);
|
|
1369
1546
|
if (hb && this.clientId != null && this.cameraId != null && hb.cid === this.clientId && hb.did === this.cameraId) {
|
|
1370
|
-
this.emit("debug", "discovery_hb_rx_connected", {
|
|
1547
|
+
this.emit("debug", "discovery_hb_rx_connected", {
|
|
1548
|
+
...hb,
|
|
1549
|
+
rhost,
|
|
1550
|
+
rport
|
|
1551
|
+
});
|
|
1371
1552
|
try {
|
|
1372
1553
|
updateRemote("d2c_hb");
|
|
1373
1554
|
this.sendHeartbeat();
|
|
@@ -1381,18 +1562,44 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1381
1562
|
try {
|
|
1382
1563
|
updateRemote("d2c_t");
|
|
1383
1564
|
this.sid = dt.sid;
|
|
1384
|
-
this.emit("debug", "discovery_t_rx_connected", {
|
|
1565
|
+
this.emit("debug", "discovery_t_rx_connected", {
|
|
1566
|
+
sid: dt.sid,
|
|
1567
|
+
cid: dt.cid,
|
|
1568
|
+
did: dt.did,
|
|
1569
|
+
rhost,
|
|
1570
|
+
rport
|
|
1571
|
+
});
|
|
1385
1572
|
const now = Date.now();
|
|
1386
1573
|
const throttleMs = 750;
|
|
1387
1574
|
if (this.lastAcceptAtMs == null || now - this.lastAcceptAtMs >= throttleMs) {
|
|
1388
|
-
const aXml = buildC2dA({
|
|
1575
|
+
const aXml = buildC2dA({
|
|
1576
|
+
sid: dt.sid,
|
|
1577
|
+
conn: dt.conn ?? "local",
|
|
1578
|
+
cid: dt.cid,
|
|
1579
|
+
did: dt.did,
|
|
1580
|
+
mtu: this.mtu
|
|
1581
|
+
});
|
|
1389
1582
|
const aPkt = encodeDiscoveryPacket(p.tid, aXml);
|
|
1390
1583
|
this.sock?.send(aPkt, rport, rhost);
|
|
1391
1584
|
this.acceptSent = true;
|
|
1392
1585
|
this.lastAcceptAtMs = now;
|
|
1393
|
-
this.emit("debug", "discovery_a_send_connected", {
|
|
1586
|
+
this.emit("debug", "discovery_a_send_connected", {
|
|
1587
|
+
sid: dt.sid,
|
|
1588
|
+
cid: dt.cid,
|
|
1589
|
+
did: dt.did,
|
|
1590
|
+
rhost,
|
|
1591
|
+
rport
|
|
1592
|
+
});
|
|
1394
1593
|
} else {
|
|
1395
|
-
this.emit("debug", "discovery_a_skip_throttle", {
|
|
1594
|
+
this.emit("debug", "discovery_a_skip_throttle", {
|
|
1595
|
+
sinceMs: now - this.lastAcceptAtMs,
|
|
1596
|
+
throttleMs,
|
|
1597
|
+
sid: dt.sid,
|
|
1598
|
+
cid: dt.cid,
|
|
1599
|
+
did: dt.did,
|
|
1600
|
+
rhost,
|
|
1601
|
+
rport
|
|
1602
|
+
});
|
|
1396
1603
|
}
|
|
1397
1604
|
} catch (e) {
|
|
1398
1605
|
this.emit("debug", "discovery_a_send_connected_error", e);
|
|
@@ -1409,10 +1616,16 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1409
1616
|
}
|
|
1410
1617
|
const disc = parseD2cDisc(p.xml);
|
|
1411
1618
|
if (disc && this.clientId != null && this.cameraId != null && disc.cid === this.clientId && disc.did === this.cameraId) {
|
|
1412
|
-
this.emit("debug", "discovery_disc_rx_connected", {
|
|
1619
|
+
this.emit("debug", "discovery_disc_rx_connected", {
|
|
1620
|
+
...disc,
|
|
1621
|
+
rhost,
|
|
1622
|
+
rport
|
|
1623
|
+
});
|
|
1413
1624
|
this.emit(
|
|
1414
1625
|
"error",
|
|
1415
|
-
new Error(
|
|
1626
|
+
new Error(
|
|
1627
|
+
`BCUDP disconnected by camera (D2C_DISC${this.sid != null ? ` sid=${this.sid}` : ""})`
|
|
1628
|
+
)
|
|
1416
1629
|
);
|
|
1417
1630
|
void this.close();
|
|
1418
1631
|
return;
|
|
@@ -1421,13 +1634,18 @@ var BcUdpStream = class extends EventEmitter {
|
|
|
1421
1634
|
return;
|
|
1422
1635
|
}
|
|
1423
1636
|
write(buf) {
|
|
1424
|
-
if (!this.sock || !this.remote || this.cameraId == null)
|
|
1637
|
+
if (!this.sock || !this.remote || this.cameraId == null)
|
|
1638
|
+
throw new Error("BCUDP stream is not connected");
|
|
1425
1639
|
const maxPayload = this.mtu - BCUDP_DATA_HEADER_SIZE;
|
|
1426
1640
|
for (let off = 0; off < buf.length; off += maxPayload) {
|
|
1427
1641
|
const payload = buf.subarray(off, Math.min(buf.length, off + maxPayload));
|
|
1428
1642
|
const packetId = this.sendPacketId >>> 0;
|
|
1429
1643
|
this.sendPacketId = this.sendPacketId + 1 >>> 0;
|
|
1430
|
-
const pkt = encodeDataPacket({
|
|
1644
|
+
const pkt = encodeDataPacket({
|
|
1645
|
+
connectionId: this.cameraId,
|
|
1646
|
+
packetId,
|
|
1647
|
+
payload: Buffer.from(payload)
|
|
1648
|
+
});
|
|
1431
1649
|
this.sent.set(packetId, { packetId, buf: pkt, ts: Date.now() });
|
|
1432
1650
|
this.sock.send(pkt, this.remote.port, this.remote.host);
|
|
1433
1651
|
}
|
|
@@ -1580,6 +1798,15 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1580
1798
|
* even if the current client instance is idle/disconnected.
|
|
1581
1799
|
*/
|
|
1582
1800
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
1801
|
+
/**
|
|
1802
|
+
* Global (process-wide) CoverPreview serialization.
|
|
1803
|
+
*
|
|
1804
|
+
* Why:
|
|
1805
|
+
* - CoverPreview uses fixed msgNum=0 and some firmwares correlate requests loosely.
|
|
1806
|
+
* - Concurrent CoverPreview attempts can cause cross-talk (400 rejects / timeouts)
|
|
1807
|
+
* and may leave device-side sessions in a bad state.
|
|
1808
|
+
*/
|
|
1809
|
+
static coverPreviewQueueTail = /* @__PURE__ */ new Map();
|
|
1583
1810
|
opts;
|
|
1584
1811
|
debugCfg;
|
|
1585
1812
|
logger;
|
|
@@ -1598,6 +1825,14 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1598
1825
|
socketClosed = false;
|
|
1599
1826
|
pendingCloseInfo;
|
|
1600
1827
|
lastDisconnectInfo;
|
|
1828
|
+
/** Tracks the most recent socket error code for inclusion in lastDisconnectInfo. */
|
|
1829
|
+
lastSocketErrorCode;
|
|
1830
|
+
/**
|
|
1831
|
+
* Promise lock to prevent parallel TCP connections.
|
|
1832
|
+
* When multiple callers race to connect, only the first one creates a socket;
|
|
1833
|
+
* others wait on the same promise.
|
|
1834
|
+
*/
|
|
1835
|
+
tcpConnectPromise;
|
|
1601
1836
|
msgNum = 0;
|
|
1602
1837
|
loggedIn = false;
|
|
1603
1838
|
// Public to allow ReolinkBaichuanApi to check login status
|
|
@@ -1689,6 +1924,23 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1689
1924
|
isIdleDisconnectEnabled() {
|
|
1690
1925
|
return this.opts.idleDisconnect === true;
|
|
1691
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Enable or disable idle disconnect dynamically.
|
|
1929
|
+
*
|
|
1930
|
+
* This is useful when the battery status is discovered after connection
|
|
1931
|
+
* (e.g., during autodetect). Call with `true` for battery cameras to
|
|
1932
|
+
* preserve battery life by disconnecting when idle.
|
|
1933
|
+
*
|
|
1934
|
+
* @param enabled - true to enable idle disconnect, false to disable
|
|
1935
|
+
*/
|
|
1936
|
+
setIdleDisconnect(enabled) {
|
|
1937
|
+
this.opts.idleDisconnect = enabled;
|
|
1938
|
+
if (enabled) {
|
|
1939
|
+
this.kickIdleDisconnectTimer();
|
|
1940
|
+
} else {
|
|
1941
|
+
this.clearIdleDisconnectTimer();
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1692
1944
|
clearIdleDisconnectTimer() {
|
|
1693
1945
|
if (!this.idleDisconnectTimer) return;
|
|
1694
1946
|
clearTimeout(this.idleDisconnectTimer);
|
|
@@ -1822,6 +2074,28 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1822
2074
|
const channel = this.opts.channel ?? 0;
|
|
1823
2075
|
return `${host}|${uid}|${channel}`;
|
|
1824
2076
|
}
|
|
2077
|
+
getCoverPreviewQueueKey() {
|
|
2078
|
+
const host = (this.opts.host ?? "").trim();
|
|
2079
|
+
const uid = (this.opts.uid ?? "").trim().toUpperCase();
|
|
2080
|
+
return `coverpreview|${host}|${uid}`;
|
|
2081
|
+
}
|
|
2082
|
+
async withSerializedCoverPreview(fn) {
|
|
2083
|
+
const key = this.getCoverPreviewQueueKey();
|
|
2084
|
+
const prevTail = _BaichuanClient.coverPreviewQueueTail.get(key) ?? Promise.resolve();
|
|
2085
|
+
const run = prevTail.catch(() => void 0).then(fn);
|
|
2086
|
+
const tail = run.then(
|
|
2087
|
+
() => void 0,
|
|
2088
|
+
() => void 0
|
|
2089
|
+
);
|
|
2090
|
+
_BaichuanClient.coverPreviewQueueTail.set(key, tail);
|
|
2091
|
+
try {
|
|
2092
|
+
return await run;
|
|
2093
|
+
} finally {
|
|
2094
|
+
if (_BaichuanClient.coverPreviewQueueTail.get(key) === tail) {
|
|
2095
|
+
_BaichuanClient.coverPreviewQueueTail.delete(key);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1825
2099
|
recomputeGlobalStreamingContribution() {
|
|
1826
2100
|
const shouldContribute = this.hasActiveVideoSubscriptionsInternal();
|
|
1827
2101
|
if (shouldContribute === this.contributesToGlobalStreamingRegistry) return;
|
|
@@ -1856,6 +2130,62 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1856
2130
|
this.emit("debug", event, data);
|
|
1857
2131
|
}
|
|
1858
2132
|
}
|
|
2133
|
+
getTcpSocketDebugSnapshot(sock) {
|
|
2134
|
+
if (!sock) return void 0;
|
|
2135
|
+
const s = sock;
|
|
2136
|
+
const snap = {
|
|
2137
|
+
destroyed: sock.destroyed,
|
|
2138
|
+
bytesRead: sock.bytesRead,
|
|
2139
|
+
bytesWritten: sock.bytesWritten
|
|
2140
|
+
};
|
|
2141
|
+
if (typeof s.readyState === "string") snap.readyState = s.readyState;
|
|
2142
|
+
if (typeof s.connecting === "boolean") snap.connecting = s.connecting;
|
|
2143
|
+
if (typeof s.pending === "boolean") snap.pending = s.pending;
|
|
2144
|
+
if (sock.localAddress != null) snap.localAddress = sock.localAddress;
|
|
2145
|
+
if (sock.localFamily != null) snap.localFamily = sock.localFamily;
|
|
2146
|
+
if (sock.localPort != null) snap.localPort = sock.localPort;
|
|
2147
|
+
if (sock.remoteAddress != null) snap.remoteAddress = sock.remoteAddress;
|
|
2148
|
+
if (sock.remoteFamily != null) snap.remoteFamily = sock.remoteFamily;
|
|
2149
|
+
if (sock.remotePort != null) snap.remotePort = sock.remotePort;
|
|
2150
|
+
if (typeof s.readableLength === "number")
|
|
2151
|
+
snap.readableLength = s.readableLength;
|
|
2152
|
+
if (typeof s.writableLength === "number")
|
|
2153
|
+
snap.writableLength = s.writableLength;
|
|
2154
|
+
if (typeof s.timeout === "number") snap.timeoutMs = s.timeout;
|
|
2155
|
+
return snap;
|
|
2156
|
+
}
|
|
2157
|
+
logSocketState(reason, extra) {
|
|
2158
|
+
try {
|
|
2159
|
+
const transport = this.transport;
|
|
2160
|
+
const sid = this.socketSessionId;
|
|
2161
|
+
const host = this.opts.host;
|
|
2162
|
+
const port = this.opts.port ?? BC_TCP_DEFAULT_PORT;
|
|
2163
|
+
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
2164
|
+
this.logDebug("socket_state", {
|
|
2165
|
+
reason,
|
|
2166
|
+
transport,
|
|
2167
|
+
host,
|
|
2168
|
+
...transport === "tcp" ? { port } : {},
|
|
2169
|
+
...sid ? { sid } : {},
|
|
2170
|
+
...shortUid ? { uid: shortUid } : {},
|
|
2171
|
+
socketClosed: this.socketClosed,
|
|
2172
|
+
socketConnected: this.isSocketConnected(),
|
|
2173
|
+
loggedIn: this.loggedIn,
|
|
2174
|
+
subscribed: this.subscribed,
|
|
2175
|
+
pending: this.pending.size,
|
|
2176
|
+
permits: this.permits.size,
|
|
2177
|
+
videoSubscriptions: this.videoSubscriptions.size,
|
|
2178
|
+
lastRxAtMs: this.lastRxAtMs,
|
|
2179
|
+
lastTxAtMs: this.lastTxAtMs,
|
|
2180
|
+
lastRxCmdId: this.lastRxInfo?.cmdId,
|
|
2181
|
+
lastTxCmdId: this.lastTxInfo?.cmdId,
|
|
2182
|
+
tcp: this.getTcpSocketDebugSnapshot(this.tcpSocket),
|
|
2183
|
+
udpConnected: this.udpSocket?.isConnected?.(),
|
|
2184
|
+
...extra
|
|
2185
|
+
});
|
|
2186
|
+
} catch {
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
1859
2189
|
getTransport() {
|
|
1860
2190
|
return this.transport;
|
|
1861
2191
|
}
|
|
@@ -1935,6 +2265,19 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1935
2265
|
getSocketSessionId() {
|
|
1936
2266
|
return this.socketSessionId;
|
|
1937
2267
|
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Get the local IP address of the socket connection.
|
|
2270
|
+
* This is the IP address that the camera sees as the client IP.
|
|
2271
|
+
*/
|
|
2272
|
+
getLocalAddress() {
|
|
2273
|
+
if (this.transport === "tcp" && this.tcpSocket) {
|
|
2274
|
+
return this.tcpSocket.localAddress;
|
|
2275
|
+
}
|
|
2276
|
+
if (this.transport === "udp" && this.udpSocket) {
|
|
2277
|
+
return this.udpSocket.localAddress?.();
|
|
2278
|
+
}
|
|
2279
|
+
return void 0;
|
|
2280
|
+
}
|
|
1938
2281
|
/**
|
|
1939
2282
|
* Check if the socket is connected and ready for operations.
|
|
1940
2283
|
* For TCP: checks if socket exists and is not destroyed.
|
|
@@ -2093,7 +2436,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2093
2436
|
if (waitMs <= 0) return;
|
|
2094
2437
|
const sid = this.socketSessionId;
|
|
2095
2438
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
2096
|
-
this.
|
|
2439
|
+
this.logDebug("udp_reconnect_cooldown", {
|
|
2097
2440
|
transport: "udp",
|
|
2098
2441
|
host: this.opts.host,
|
|
2099
2442
|
sid,
|
|
@@ -2103,6 +2446,33 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2103
2446
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
2104
2447
|
}
|
|
2105
2448
|
async connectTcp() {
|
|
2449
|
+
if (this.tcpConnectPromise) {
|
|
2450
|
+
this.logDebug(
|
|
2451
|
+
"tcp_connect_waiting",
|
|
2452
|
+
"Waiting for existing connection promise"
|
|
2453
|
+
);
|
|
2454
|
+
await this.tcpConnectPromise;
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
if (this.tcpSocket && !this.tcpSocket.destroyed) {
|
|
2458
|
+
this.transport = "tcp";
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
this.logDebug("tcp_connect_new", {
|
|
2462
|
+
hasSocket: !!this.tcpSocket,
|
|
2463
|
+
destroyed: this.tcpSocket?.destroyed
|
|
2464
|
+
});
|
|
2465
|
+
this.tcpConnectPromise = this.doConnectTcp();
|
|
2466
|
+
try {
|
|
2467
|
+
await this.tcpConnectPromise;
|
|
2468
|
+
} finally {
|
|
2469
|
+
this.tcpConnectPromise = void 0;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Internal TCP connection logic. Only called from connectTcp() with promise lock.
|
|
2474
|
+
*/
|
|
2475
|
+
async doConnectTcp() {
|
|
2106
2476
|
if (this.tcpSocket && !this.tcpSocket.destroyed) {
|
|
2107
2477
|
this.transport = "tcp";
|
|
2108
2478
|
return;
|
|
@@ -2122,7 +2492,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2122
2492
|
const frames = this.parser.push(chunk);
|
|
2123
2493
|
for (const f of frames) this.handleFrame(f);
|
|
2124
2494
|
});
|
|
2125
|
-
sock.on("close", () => {
|
|
2495
|
+
sock.on("close", (hadError) => {
|
|
2496
|
+
this.logSocketState("tcp_close_event", { hadError: Boolean(hadError) });
|
|
2126
2497
|
const sid2 = this.socketSessionId;
|
|
2127
2498
|
if (sock === this.tcpSocket) {
|
|
2128
2499
|
this.tcpSocket = void 0;
|
|
@@ -2135,11 +2506,14 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2135
2506
|
this.socketClosed = true;
|
|
2136
2507
|
const pending = this.pendingCloseInfo;
|
|
2137
2508
|
this.pendingCloseInfo = void 0;
|
|
2509
|
+
const errorCode = this.lastSocketErrorCode;
|
|
2510
|
+
this.lastSocketErrorCode = void 0;
|
|
2138
2511
|
this.lastDisconnectInfo = {
|
|
2139
2512
|
atMs: Date.now(),
|
|
2140
2513
|
transport: "tcp",
|
|
2141
2514
|
voluntary: pending != null,
|
|
2142
|
-
reason: pending?.reason ?? "socket_closed"
|
|
2515
|
+
reason: pending?.reason ?? "socket_closed",
|
|
2516
|
+
...errorCode != null && { errorCode }
|
|
2143
2517
|
};
|
|
2144
2518
|
const tcpDisconnectParts = [
|
|
2145
2519
|
`transport=tcp`,
|
|
@@ -2168,7 +2542,17 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2168
2542
|
});
|
|
2169
2543
|
}
|
|
2170
2544
|
});
|
|
2171
|
-
sock.on("error", (err) =>
|
|
2545
|
+
sock.on("error", (err) => {
|
|
2546
|
+
const code = err?.code;
|
|
2547
|
+
this.lastSocketErrorCode = code;
|
|
2548
|
+
this.logSocketState("tcp_error", {
|
|
2549
|
+
error: {
|
|
2550
|
+
message: err?.message ?? String(err),
|
|
2551
|
+
code
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
this.emit("error", err);
|
|
2555
|
+
});
|
|
2172
2556
|
await new Promise((resolve, reject) => {
|
|
2173
2557
|
sock.once("connect", () => resolve());
|
|
2174
2558
|
sock.once("error", (e) => reject(e));
|
|
@@ -2186,6 +2570,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2186
2570
|
"connected",
|
|
2187
2571
|
`transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
|
|
2188
2572
|
);
|
|
2573
|
+
this.logSocketState("tcp_connected");
|
|
2189
2574
|
this.startKeepAlive();
|
|
2190
2575
|
this.kickIdleDisconnectTimer();
|
|
2191
2576
|
}
|
|
@@ -2202,13 +2587,18 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2202
2587
|
this.udpSocket = void 0;
|
|
2203
2588
|
}
|
|
2204
2589
|
if (!this.opts.uid) {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
)
|
|
2590
|
+
const isLocalDirect = this.opts.udpDiscoveryMethod === "local-direct";
|
|
2591
|
+
const hasDirectHost = !!this.opts.host?.trim();
|
|
2592
|
+
if (!isLocalDirect || !hasDirectHost) {
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
"Baichuan UDP requested but `options.uid` is not set (required for BCUDP discovery unless using local-direct with a direct host)."
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2208
2597
|
}
|
|
2209
2598
|
const sock = new BcUdpStream({
|
|
2210
2599
|
mode: "uid",
|
|
2211
|
-
|
|
2600
|
+
// UID may be empty for local-direct with directHost
|
|
2601
|
+
uid: this.opts.uid ?? "",
|
|
2212
2602
|
// If the camera IP/hostname is known, allow local-direct to try unicast before broadcast.
|
|
2213
2603
|
...this.opts.host?.trim() ? { directHost: this.opts.host.trim() } : {},
|
|
2214
2604
|
...this.opts.udpDiscoveryMethod ? { discoveryMethod: this.opts.udpDiscoveryMethod } : {}
|
|
@@ -2222,6 +2612,9 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2222
2612
|
for (const f of frames) this.handleFrame(f);
|
|
2223
2613
|
});
|
|
2224
2614
|
sock.on("close", () => {
|
|
2615
|
+
this.logSocketState("udp_close_event", {
|
|
2616
|
+
udpConnected: sock.isConnected?.()
|
|
2617
|
+
});
|
|
2225
2618
|
const sid2 = this.socketSessionId;
|
|
2226
2619
|
if (sock === this.udpSocket) {
|
|
2227
2620
|
this.udpSocket = void 0;
|
|
@@ -2233,11 +2626,14 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2233
2626
|
this.socketClosed = true;
|
|
2234
2627
|
const pending = this.pendingCloseInfo;
|
|
2235
2628
|
this.pendingCloseInfo = void 0;
|
|
2629
|
+
const errorCode = this.lastSocketErrorCode;
|
|
2630
|
+
this.lastSocketErrorCode = void 0;
|
|
2236
2631
|
this.lastDisconnectInfo = {
|
|
2237
2632
|
atMs: Date.now(),
|
|
2238
2633
|
transport: "udp",
|
|
2239
2634
|
voluntary: pending != null,
|
|
2240
|
-
reason: pending?.reason ?? "socket_closed"
|
|
2635
|
+
reason: pending?.reason ?? "socket_closed",
|
|
2636
|
+
...errorCode != null && { errorCode }
|
|
2241
2637
|
};
|
|
2242
2638
|
const udpDisconnectParts = [
|
|
2243
2639
|
`transport=udp`,
|
|
@@ -2274,6 +2670,10 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2274
2670
|
});
|
|
2275
2671
|
sock.on("error", (err) => {
|
|
2276
2672
|
if (err?.message?.includes("D2C_DISC")) {
|
|
2673
|
+
this.logSocketState("udp_error_d2c_disc", {
|
|
2674
|
+
message: err.message,
|
|
2675
|
+
udpConnected: sock.isConnected?.()
|
|
2676
|
+
});
|
|
2277
2677
|
const now = Date.now();
|
|
2278
2678
|
const sid2 = this.socketSessionId;
|
|
2279
2679
|
const shortUid2 = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
@@ -2300,7 +2700,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2300
2700
|
this.udpReconnectCooldownUntilMs,
|
|
2301
2701
|
now + nextBackoffMs
|
|
2302
2702
|
);
|
|
2303
|
-
this.
|
|
2703
|
+
this.logDebug("d2c_disc_backoff", {
|
|
2304
2704
|
transport: "udp",
|
|
2305
2705
|
host: this.opts.host,
|
|
2306
2706
|
sid: sid2,
|
|
@@ -2330,9 +2730,64 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2330
2730
|
"connected",
|
|
2331
2731
|
`transport=udp host=${this.opts.host}${sid ? ` sid=${sid}` : ""}${udpRemoteHost ? ` remote=${udpRemoteHost}` : ""} uid=${shortUid} udpDiscoveryMethod=${udpDiscoveryMethod}`
|
|
2332
2732
|
);
|
|
2733
|
+
this.logSocketState("udp_connected", {
|
|
2734
|
+
udpConnected: sock.isConnected?.(),
|
|
2735
|
+
udpRemoteHost,
|
|
2736
|
+
udpDiscoveryMethod
|
|
2737
|
+
});
|
|
2333
2738
|
this.startKeepAlive();
|
|
2334
2739
|
this.kickIdleDisconnectTimer();
|
|
2335
2740
|
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Send a logout command to the camera to gracefully end the session.
|
|
2743
|
+
*
|
|
2744
|
+
* This notifies the camera that we're done, preventing "hung" sessions
|
|
2745
|
+
* that would otherwise wait for a timeout on the camera side.
|
|
2746
|
+
*
|
|
2747
|
+
* PCAP-confirmed: cmdId=2 with encrypted XML body.
|
|
2748
|
+
* Call this before close() for clean session termination.
|
|
2749
|
+
*
|
|
2750
|
+
* @returns true if logout was sent and acknowledged, false if failed or not logged in
|
|
2751
|
+
*/
|
|
2752
|
+
async logout() {
|
|
2753
|
+
if (!this.loggedIn || !this.isSocketConnected()) {
|
|
2754
|
+
this.logDebug("logout_skip", {
|
|
2755
|
+
loggedIn: this.loggedIn,
|
|
2756
|
+
socketConnected: this.isSocketConnected()
|
|
2757
|
+
});
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
try {
|
|
2761
|
+
this.logDebug("logout_start", { host: this.opts.host });
|
|
2762
|
+
const logoutXml = buildLogoutXml();
|
|
2763
|
+
const effectiveHostChannelId = this.hostChannelId;
|
|
2764
|
+
const response = await this.sendFrame({
|
|
2765
|
+
cmdId: BC_CMD_ID_LOGOUT,
|
|
2766
|
+
payloadXml: logoutXml,
|
|
2767
|
+
extensionXml: "",
|
|
2768
|
+
messageClass: BC_CLASS_MODERN_24,
|
|
2769
|
+
channelIdOverride: effectiveHostChannelId,
|
|
2770
|
+
timeoutMs: 5e3
|
|
2771
|
+
// Short timeout - camera should respond quickly
|
|
2772
|
+
});
|
|
2773
|
+
const responseCode = response.header.responseCode;
|
|
2774
|
+
const success = responseCode === 200;
|
|
2775
|
+
this.logDebug("logout_response", {
|
|
2776
|
+
responseCode,
|
|
2777
|
+
success
|
|
2778
|
+
});
|
|
2779
|
+
this.loggedIn = false;
|
|
2780
|
+
this.nonce = void 0;
|
|
2781
|
+
return success;
|
|
2782
|
+
} catch (e) {
|
|
2783
|
+
this.logDebug("logout_error", {
|
|
2784
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2785
|
+
});
|
|
2786
|
+
this.loggedIn = false;
|
|
2787
|
+
this.nonce = void 0;
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2336
2791
|
async close(options) {
|
|
2337
2792
|
const hasSocket = Boolean(
|
|
2338
2793
|
this.tcpSocket && !this.tcpSocket.destroyed || this.udpSocket
|
|
@@ -2350,7 +2805,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2350
2805
|
const host = this.opts.host;
|
|
2351
2806
|
const port = this.opts.port ?? BC_TCP_DEFAULT_PORT;
|
|
2352
2807
|
const shortUid = this.opts.uid ? this.opts.uid.substring(0, 5) : void 0;
|
|
2353
|
-
this.
|
|
2808
|
+
this.logDebug("closing", {
|
|
2354
2809
|
transport,
|
|
2355
2810
|
host,
|
|
2356
2811
|
...transport === "tcp" ? { port } : {},
|
|
@@ -2365,15 +2820,26 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2365
2820
|
permits: this.permits.size,
|
|
2366
2821
|
videoSubscriptions: this.videoSubscriptions.size
|
|
2367
2822
|
});
|
|
2823
|
+
this.logSocketState("close_called", { reason });
|
|
2368
2824
|
if ((process.env.BAICHUAN_LOG_CLOSE_STACK ?? "").trim() === "1") {
|
|
2369
2825
|
const stack = new Error("BaichuanClient.close() stack").stack;
|
|
2370
2826
|
if (stack) {
|
|
2371
2827
|
const lines = stack.split("\n").slice(0, 10).join("\n");
|
|
2372
|
-
this.
|
|
2828
|
+
this.logDebug("closing_stack", lines);
|
|
2373
2829
|
}
|
|
2374
2830
|
}
|
|
2375
2831
|
} catch {
|
|
2376
2832
|
}
|
|
2833
|
+
const skipLogout = options?.skipLogout ?? false;
|
|
2834
|
+
if (!skipLogout && this.loggedIn && this.isSocketConnected()) {
|
|
2835
|
+
try {
|
|
2836
|
+
await this.logout();
|
|
2837
|
+
} catch (e) {
|
|
2838
|
+
this.logDebug("close_logout_error", {
|
|
2839
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2377
2843
|
this.stopKeepAlive();
|
|
2378
2844
|
this.clearIdleDisconnectTimer();
|
|
2379
2845
|
for (const id of Array.from(this.permits.keys())) {
|
|
@@ -3940,27 +4406,31 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
3940
4406
|
* instead of JPEG.
|
|
3941
4407
|
*/
|
|
3942
4408
|
async sendBinaryCoverPreview(params) {
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
4409
|
+
return await this.withSerializedCoverPreview(async () => {
|
|
4410
|
+
const maxRetries = params.maxRetries ?? 5;
|
|
4411
|
+
const retryDelayMs = params.retryDelayMs ?? 1e3;
|
|
4412
|
+
let lastError;
|
|
4413
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
4414
|
+
try {
|
|
4415
|
+
return await this._sendBinaryCoverPreviewOnce(params);
|
|
4416
|
+
} catch (e) {
|
|
4417
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4418
|
+
lastError = e instanceof Error ? e : new Error(msg);
|
|
4419
|
+
const is400Rejection = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
|
|
4420
|
+
if (is400Rejection && attempt < maxRetries - 1) {
|
|
4421
|
+
this.logDebug("coverpreview_retry_400", {
|
|
4422
|
+
attempt: attempt + 1,
|
|
4423
|
+
maxRetries,
|
|
4424
|
+
retryDelayMs
|
|
4425
|
+
});
|
|
4426
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
4427
|
+
continue;
|
|
4428
|
+
}
|
|
4429
|
+
throw lastError;
|
|
3959
4430
|
}
|
|
3960
|
-
throw lastError;
|
|
3961
4431
|
}
|
|
3962
|
-
|
|
3963
|
-
|
|
4432
|
+
throw lastError ?? new Error("CoverPreview failed after all retries");
|
|
4433
|
+
});
|
|
3964
4434
|
}
|
|
3965
4435
|
/**
|
|
3966
4436
|
* Internal: single attempt for sendBinaryCoverPreview
|
|
@@ -4063,8 +4533,16 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
4063
4533
|
return await new Promise((resolve, reject) => {
|
|
4064
4534
|
let timeout;
|
|
4065
4535
|
let done = false;
|
|
4536
|
+
const onClose = () => {
|
|
4537
|
+
fail(
|
|
4538
|
+
new Error(
|
|
4539
|
+
`Baichuan socket closed while waiting CoverPreview cmdId=${cmdId} msgNum=${msgNum}`
|
|
4540
|
+
)
|
|
4541
|
+
);
|
|
4542
|
+
};
|
|
4066
4543
|
const cleanup = () => {
|
|
4067
4544
|
this.off("frame", onFrame);
|
|
4545
|
+
this.off("close", onClose);
|
|
4068
4546
|
if (timeout) clearTimeout(timeout);
|
|
4069
4547
|
};
|
|
4070
4548
|
const finish = (buf) => {
|
|
@@ -4183,6 +4661,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
4183
4661
|
}
|
|
4184
4662
|
}, timeoutMs);
|
|
4185
4663
|
this.on("frame", onFrame);
|
|
4664
|
+
this.on("close", onClose);
|
|
4186
4665
|
try {
|
|
4187
4666
|
this.logDebug("tx", {
|
|
4188
4667
|
cmdId,
|
|
@@ -8261,7 +8740,10 @@ var logDebugStreamBlock = (params) => {
|
|
|
8261
8740
|
const { logger, traceNativeStream, channel, tag, blockXml } = params;
|
|
8262
8741
|
if (!traceNativeStream) return;
|
|
8263
8742
|
if (!blockXml) {
|
|
8264
|
-
(logger.warn ?? logger.log).call(
|
|
8743
|
+
(logger.warn ?? logger.log).call(
|
|
8744
|
+
logger,
|
|
8745
|
+
`[ReolinkBaichuanApi] getStreamMetadata(traceNativeStream): channel=${channel} tag=<${tag}> missing`
|
|
8746
|
+
);
|
|
8265
8747
|
return;
|
|
8266
8748
|
}
|
|
8267
8749
|
const raw = blockXml;
|
|
@@ -8297,7 +8779,8 @@ var buildStream = (params) => {
|
|
|
8297
8779
|
const audio = Number(getXmlText(streamXml, "audio") ?? "0");
|
|
8298
8780
|
const enabled = getXmlText(streamXml, "enable");
|
|
8299
8781
|
const isEnabled = isEnabledFromText(enabled);
|
|
8300
|
-
if (!isEnabled || !isPlausibleStream({ width, height, frameRate, bitRate }))
|
|
8782
|
+
if (!isEnabled || !isPlausibleStream({ width, height, frameRate, bitRate }))
|
|
8783
|
+
return void 0;
|
|
8301
8784
|
return {
|
|
8302
8785
|
profile,
|
|
8303
8786
|
audio,
|
|
@@ -8335,7 +8818,13 @@ var parseChannelStreamMetadataFromGetEncXml = (params) => {
|
|
|
8335
8818
|
const mainMatch = xml.match(/<mainStream[^>]*>([\s\S]*?)<\/mainStream>/);
|
|
8336
8819
|
if (mainMatch) {
|
|
8337
8820
|
const mainXml = mainMatch[1] ?? "";
|
|
8338
|
-
logDebugStreamBlock({
|
|
8821
|
+
logDebugStreamBlock({
|
|
8822
|
+
logger,
|
|
8823
|
+
traceNativeStream,
|
|
8824
|
+
channel,
|
|
8825
|
+
tag: "mainStream",
|
|
8826
|
+
blockXml: mainXml
|
|
8827
|
+
});
|
|
8339
8828
|
const s = buildStream({ profile: "main", streamXml: mainXml });
|
|
8340
8829
|
if (s) {
|
|
8341
8830
|
streams.push(s);
|
|
@@ -8345,19 +8834,40 @@ var parseChannelStreamMetadataFromGetEncXml = (params) => {
|
|
|
8345
8834
|
const subMatch = xml.match(/<subStream[^>]*>([\s\S]*?)<\/subStream>/);
|
|
8346
8835
|
if (subMatch) {
|
|
8347
8836
|
const subXml = subMatch[1] ?? "";
|
|
8348
|
-
logDebugStreamBlock({
|
|
8837
|
+
logDebugStreamBlock({
|
|
8838
|
+
logger,
|
|
8839
|
+
traceNativeStream,
|
|
8840
|
+
channel,
|
|
8841
|
+
tag: "subStream",
|
|
8842
|
+
blockXml: subXml
|
|
8843
|
+
});
|
|
8349
8844
|
const s = buildStream({ profile: "sub", streamXml: subXml });
|
|
8350
8845
|
if (s) {
|
|
8351
8846
|
streams.push(s);
|
|
8352
8847
|
audioEnabled = audioEnabled && s.audio === 1;
|
|
8353
8848
|
}
|
|
8354
8849
|
}
|
|
8355
|
-
const extLikeTags = [
|
|
8850
|
+
const extLikeTags = [
|
|
8851
|
+
"extStream",
|
|
8852
|
+
"thirdStream",
|
|
8853
|
+
"externStream",
|
|
8854
|
+
"extraStream"
|
|
8855
|
+
];
|
|
8856
|
+
let extTagFound;
|
|
8356
8857
|
for (const tag of extLikeTags) {
|
|
8357
|
-
const extMatch = xml.match(
|
|
8858
|
+
const extMatch = xml.match(
|
|
8859
|
+
new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`)
|
|
8860
|
+
);
|
|
8358
8861
|
if (!extMatch) continue;
|
|
8862
|
+
extTagFound = tag;
|
|
8359
8863
|
const extXml = extMatch[1] ?? "";
|
|
8360
|
-
logDebugStreamBlock({
|
|
8864
|
+
logDebugStreamBlock({
|
|
8865
|
+
logger,
|
|
8866
|
+
traceNativeStream,
|
|
8867
|
+
channel,
|
|
8868
|
+
tag,
|
|
8869
|
+
blockXml: extXml
|
|
8870
|
+
});
|
|
8361
8871
|
const s = buildStream({ profile: "ext", streamXml: extXml });
|
|
8362
8872
|
if (s) {
|
|
8363
8873
|
streams.push(s);
|
|
@@ -8368,7 +8878,8 @@ var parseChannelStreamMetadataFromGetEncXml = (params) => {
|
|
|
8368
8878
|
return {
|
|
8369
8879
|
channel,
|
|
8370
8880
|
streams,
|
|
8371
|
-
audioEnabled
|
|
8881
|
+
audioEnabled,
|
|
8882
|
+
rawXml: xml
|
|
8372
8883
|
};
|
|
8373
8884
|
};
|
|
8374
8885
|
|
|
@@ -8975,7 +9486,6 @@ var isNvrHubModel = (model) => {
|
|
|
8975
9486
|
return NVR_HUB_MODEL_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
8976
9487
|
};
|
|
8977
9488
|
var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
8978
|
-
client;
|
|
8979
9489
|
logger;
|
|
8980
9490
|
httpClient;
|
|
8981
9491
|
cgiApi;
|
|
@@ -8983,6 +9493,39 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
8983
9493
|
host;
|
|
8984
9494
|
username;
|
|
8985
9495
|
password;
|
|
9496
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9497
|
+
// SOCKET POOL - Tag-based socket management
|
|
9498
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9499
|
+
/**
|
|
9500
|
+
* Socket pool with tag-based allocation strategy.
|
|
9501
|
+
* Tags determine which sockets are shared vs dedicated:
|
|
9502
|
+
*
|
|
9503
|
+
* For standalone camera (channelCount=1):
|
|
9504
|
+
* - "general" - commands, events, ext stream (all share one socket)
|
|
9505
|
+
* - "streaming" - main + sub streams (share one socket)
|
|
9506
|
+
* - "replay:XXX" - dedicated per replay session
|
|
9507
|
+
*
|
|
9508
|
+
* For NVR (channelCount>1):
|
|
9509
|
+
* - "general" - commands, events (shared socket)
|
|
9510
|
+
* - "streaming:chN" - main + sub for channel N (one socket per channel)
|
|
9511
|
+
* - "replay:XXX" - dedicated per replay session
|
|
9512
|
+
*/
|
|
9513
|
+
socketPool = /* @__PURE__ */ new Map();
|
|
9514
|
+
/** BaichuanClientOptions to use when creating new sockets */
|
|
9515
|
+
clientOptions;
|
|
9516
|
+
/**
|
|
9517
|
+
* Get the primary "general" socket. This is the default socket for commands and events.
|
|
9518
|
+
* Lazily created on first access if not already initialized.
|
|
9519
|
+
*
|
|
9520
|
+
* This getter maintains backward compatibility with existing code that uses `this.client`.
|
|
9521
|
+
*/
|
|
9522
|
+
get client() {
|
|
9523
|
+
const entry = this.socketPool.get("general");
|
|
9524
|
+
if (!entry) {
|
|
9525
|
+
throw new Error("[ReolinkBaichuanApi] General socket not initialized");
|
|
9526
|
+
}
|
|
9527
|
+
return entry.client;
|
|
9528
|
+
}
|
|
8986
9529
|
/**
|
|
8987
9530
|
* Cached camera UID. May be initially undefined if not provided in the constructor.
|
|
8988
9531
|
* Will be lazily populated on demand when needed (e.g. for recordings).
|
|
@@ -8995,10 +9538,36 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
8995
9538
|
* Set during login or when capabilities are queried.
|
|
8996
9539
|
*/
|
|
8997
9540
|
_channelCount;
|
|
8998
|
-
|
|
9541
|
+
/**
|
|
9542
|
+
* Cached NVR/Hub detection result.
|
|
9543
|
+
* - true = NVR/Hub with multiple channels
|
|
9544
|
+
* - false = standalone camera
|
|
9545
|
+
* Set via setIsNvr() from the plugin or auto-detected via isNvrDevice().
|
|
9546
|
+
*/
|
|
9547
|
+
_isNvr;
|
|
9548
|
+
/** Maximum dedicated sessions allowed before triggering a reboot (default: 7). */
|
|
9549
|
+
maxDedicatedSessionsBeforeReboot;
|
|
9550
|
+
sessionGuardRebootInFlight;
|
|
9551
|
+
sessionGuardLastRebootAtMs;
|
|
9552
|
+
/** Track last known session count and IDs for change detection. */
|
|
9553
|
+
lastKnownSessionCount;
|
|
9554
|
+
lastKnownSessionIds = /* @__PURE__ */ new Set();
|
|
9555
|
+
/** Reboot if too many voluntary disconnections per minute (default: 15). */
|
|
9556
|
+
rebootAfterDisconnectionsPerMinute = 15;
|
|
8999
9557
|
disconnectStormVoluntaryAtMs = [];
|
|
9000
9558
|
disconnectStormRebootInFlight;
|
|
9001
9559
|
disconnectStormLastRebootAtMs;
|
|
9560
|
+
/**
|
|
9561
|
+
* ECONNRESET storm guard: reboot if too many consecutive ECONNRESET errors.
|
|
9562
|
+
* Default threshold: 10 consecutive ECONNRESET within 60 seconds.
|
|
9563
|
+
*/
|
|
9564
|
+
rebootAfterConsecutiveEconnreset = 10;
|
|
9565
|
+
consecutiveEconnresetCount = 0;
|
|
9566
|
+
consecutiveEconnresetFirstAtMs;
|
|
9567
|
+
econnresetStormRebootInFlight;
|
|
9568
|
+
econnresetStormLastRebootAtMs;
|
|
9569
|
+
/** Periodic session check interval (every 60 seconds). */
|
|
9570
|
+
sessionGuardIntervalTimer;
|
|
9002
9571
|
simpleEventListeners = /* @__PURE__ */ new Set();
|
|
9003
9572
|
simpleEventSubscribed = false;
|
|
9004
9573
|
simpleEventSubscribeInFlight;
|
|
@@ -9027,29 +9596,68 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9027
9596
|
deviceCapabilitiesCache = /* @__PURE__ */ new Map();
|
|
9028
9597
|
static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
9029
9598
|
// 5 minutes
|
|
9599
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9600
|
+
// SOCKET POOL CONSTANTS
|
|
9601
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9602
|
+
/** Keep replay/streaming sockets warm briefly to reduce clip switch latency. */
|
|
9603
|
+
static SOCKET_POOL_KEEPALIVE_MS = 1e4;
|
|
9030
9604
|
/**
|
|
9031
|
-
*
|
|
9032
|
-
*
|
|
9033
|
-
*
|
|
9034
|
-
*
|
|
9035
|
-
* Key: unique session ID (e.g., "replay:channel:fileName")
|
|
9036
|
-
* Value: client, refCount, createdAt
|
|
9605
|
+
* Cooldown tracking to prevent session spam when login repeatedly fails.
|
|
9606
|
+
* Key: host address (since camera session limits are per-device, not per-sessionKey)
|
|
9607
|
+
* Value: object with failureCount, lastFailureAt, cooldownUntil
|
|
9037
9608
|
*/
|
|
9038
|
-
|
|
9039
|
-
/**
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
static
|
|
9609
|
+
socketPoolCooldowns = /* @__PURE__ */ new Map();
|
|
9610
|
+
/** Base cooldown duration (ms) for socket failures. */
|
|
9611
|
+
static SOCKET_POOL_BASE_COOLDOWN_MS = 5e3;
|
|
9612
|
+
/** Max cooldown duration (ms) - caps exponential backoff. */
|
|
9613
|
+
static SOCKET_POOL_MAX_COOLDOWN_MS = 12e4;
|
|
9614
|
+
/** Failure count threshold before entering cooldown. */
|
|
9615
|
+
static SOCKET_POOL_FAILURE_THRESHOLD = 2;
|
|
9616
|
+
/** Time window (ms) to reset failure count if no failures occur. */
|
|
9617
|
+
static SOCKET_POOL_FAILURE_WINDOW_MS = 6e4;
|
|
9043
9618
|
/**
|
|
9044
|
-
* Get a summary of currently active
|
|
9619
|
+
* Get a summary of currently active sockets in the pool.
|
|
9045
9620
|
* Useful for debugging/logging to see how many sockets are open.
|
|
9046
9621
|
*/
|
|
9622
|
+
getSocketPoolSummary() {
|
|
9623
|
+
return {
|
|
9624
|
+
count: this.socketPool.size,
|
|
9625
|
+
tags: Array.from(this.socketPool.keys())
|
|
9626
|
+
};
|
|
9627
|
+
}
|
|
9628
|
+
/**
|
|
9629
|
+
* @deprecated Use getSocketPoolSummary() instead.
|
|
9630
|
+
*/
|
|
9047
9631
|
getDedicatedSessionsSummary() {
|
|
9632
|
+
const summary = this.getSocketPoolSummary();
|
|
9048
9633
|
return {
|
|
9049
|
-
count:
|
|
9050
|
-
keys:
|
|
9634
|
+
count: summary.count,
|
|
9635
|
+
keys: summary.tags
|
|
9051
9636
|
};
|
|
9052
9637
|
}
|
|
9638
|
+
/**
|
|
9639
|
+
* Get cooldown status for socket pool connections.
|
|
9640
|
+
* Useful for debugging when connections are being rate-limited.
|
|
9641
|
+
*/
|
|
9642
|
+
getSocketPoolCooldownStatus() {
|
|
9643
|
+
const entry = this.socketPoolCooldowns.get(this.host);
|
|
9644
|
+
if (!entry) return null;
|
|
9645
|
+
const now = Date.now();
|
|
9646
|
+
const inCooldown = now < entry.cooldownUntil;
|
|
9647
|
+
return {
|
|
9648
|
+
host: this.host,
|
|
9649
|
+
inCooldown,
|
|
9650
|
+
failureCount: entry.failureCount,
|
|
9651
|
+
cooldownRemainingMs: inCooldown ? entry.cooldownUntil - now : 0,
|
|
9652
|
+
cooldownUntil: inCooldown ? new Date(entry.cooldownUntil) : null
|
|
9653
|
+
};
|
|
9654
|
+
}
|
|
9655
|
+
/**
|
|
9656
|
+
* @deprecated Use getSocketPoolCooldownStatus() instead.
|
|
9657
|
+
*/
|
|
9658
|
+
getDedicatedClientCooldownStatus() {
|
|
9659
|
+
return this.getSocketPoolCooldownStatus();
|
|
9660
|
+
}
|
|
9053
9661
|
/**
|
|
9054
9662
|
* Cached per-channel data from cmd_id 145 push (NVR sends this automatically on connection).
|
|
9055
9663
|
*
|
|
@@ -9135,6 +9743,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9135
9743
|
*/
|
|
9136
9744
|
replayQueue = [];
|
|
9137
9745
|
replayQueueProcessing = false;
|
|
9746
|
+
/**
|
|
9747
|
+
* Abort controller for the currently active replay stream.
|
|
9748
|
+
* When a new clip is requested, we signal the current one to stop.
|
|
9749
|
+
*/
|
|
9750
|
+
activeReplayAbortController = null;
|
|
9138
9751
|
/** Minimum delay between replay operations to give camera time to reset */
|
|
9139
9752
|
REPLAY_COOLDOWN_MS = 500;
|
|
9140
9753
|
lastReplayEndTime = 0;
|
|
@@ -9226,17 +9839,23 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9226
9839
|
* continues producing data after the initial setup.
|
|
9227
9840
|
*
|
|
9228
9841
|
* @param setup - Function that sets up the stream. Called when it's this operation's turn.
|
|
9229
|
-
*
|
|
9842
|
+
* Receives an AbortSignal that will be triggered if a new clip is requested.
|
|
9843
|
+
* @returns Promise that resolves when setup is complete, with the result, release function, and abort signal.
|
|
9230
9844
|
*/
|
|
9231
9845
|
enqueueStreamingReplayOperation(setup) {
|
|
9846
|
+
if (this.activeReplayAbortController) {
|
|
9847
|
+
this.logger?.debug?.(
|
|
9848
|
+
"[ReplayQueue] Signaling current replay stream to abort for new clip"
|
|
9849
|
+
);
|
|
9850
|
+
this.activeReplayAbortController.abort();
|
|
9851
|
+
this.activeReplayAbortController = null;
|
|
9852
|
+
}
|
|
9232
9853
|
let resolvePromise;
|
|
9233
9854
|
let rejectPromise;
|
|
9234
|
-
const promise = new Promise(
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
}
|
|
9239
|
-
);
|
|
9855
|
+
const promise = new Promise((resolve, reject) => {
|
|
9856
|
+
resolvePromise = resolve;
|
|
9857
|
+
rejectPromise = reject;
|
|
9858
|
+
});
|
|
9240
9859
|
this.replayQueue.push({
|
|
9241
9860
|
execute: () => {
|
|
9242
9861
|
return new Promise((releaseSlot) => {
|
|
@@ -9244,26 +9863,33 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9244
9863
|
const safeRelease = () => {
|
|
9245
9864
|
if (released) return;
|
|
9246
9865
|
released = true;
|
|
9866
|
+
if (this.activeReplayAbortController && this.activeReplayAbortController === abortController) {
|
|
9867
|
+
this.activeReplayAbortController = null;
|
|
9868
|
+
}
|
|
9247
9869
|
releaseSlot();
|
|
9248
9870
|
};
|
|
9871
|
+
const abortController = new AbortController();
|
|
9872
|
+
this.activeReplayAbortController = abortController;
|
|
9249
9873
|
const safetyTimeout = setTimeout(
|
|
9250
9874
|
() => {
|
|
9251
9875
|
if (!released) {
|
|
9252
9876
|
this.logger?.warn?.(
|
|
9253
9877
|
"[ReplayQueue] Safety timeout: releasing queue slot after 10 minutes"
|
|
9254
9878
|
);
|
|
9879
|
+
abortController.abort();
|
|
9255
9880
|
safeRelease();
|
|
9256
9881
|
}
|
|
9257
9882
|
},
|
|
9258
9883
|
10 * 60 * 1e3
|
|
9259
9884
|
);
|
|
9260
|
-
setup().then((result) => {
|
|
9885
|
+
setup(abortController.signal).then((result) => {
|
|
9261
9886
|
resolvePromise({
|
|
9262
9887
|
result,
|
|
9263
9888
|
release: () => {
|
|
9264
9889
|
clearTimeout(safetyTimeout);
|
|
9265
9890
|
safeRelease();
|
|
9266
|
-
}
|
|
9891
|
+
},
|
|
9892
|
+
abortSignal: abortController.signal
|
|
9267
9893
|
});
|
|
9268
9894
|
}).catch((e) => {
|
|
9269
9895
|
clearTimeout(safetyTimeout);
|
|
@@ -9326,32 +9952,95 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9326
9952
|
});
|
|
9327
9953
|
}
|
|
9328
9954
|
recordingsCacheTtlMs = 20 * 60 * 1e3;
|
|
9955
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9956
|
+
// SOCKET POOL MANAGEMENT
|
|
9957
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9329
9958
|
/**
|
|
9330
|
-
*
|
|
9331
|
-
*
|
|
9959
|
+
* Determine the socket tag for a given sessionKey.
|
|
9960
|
+
* This implements the tag-based allocation strategy:
|
|
9961
|
+
*
|
|
9962
|
+
* - "general" - commands, events
|
|
9963
|
+
* - "streaming:ch{N}" - main + sub for channel N (closed when no streams)
|
|
9964
|
+
* - "streaming:ch{N}:ext" - ext for channel N (closed when no streams)
|
|
9965
|
+
* - "replay:deviceId:ch{N}" - dedicated per device+channel for replay
|
|
9332
9966
|
*
|
|
9333
|
-
*
|
|
9334
|
-
*
|
|
9967
|
+
* Always uses per-channel tagging for streams (works for both standalone and NVR).
|
|
9968
|
+
* Replay uses per-device+channel sockets to allow multiple users to watch
|
|
9969
|
+
* different clips simultaneously without interfering with each other.
|
|
9335
9970
|
*
|
|
9336
|
-
*
|
|
9337
|
-
*
|
|
9338
|
-
* immediately and create a new one. This ensures clean state for each clip.
|
|
9971
|
+
* @param sessionKey - The session key (e.g., "live:device:ch0:main", "replay:device:ch1:file")
|
|
9972
|
+
* @returns The socket pool tag to use
|
|
9339
9973
|
*/
|
|
9340
|
-
|
|
9974
|
+
resolveSocketTag(sessionKey) {
|
|
9975
|
+
const replayMatch = sessionKey.match(/^replay:([^:]+)(?::ch(\d+))?/);
|
|
9976
|
+
if (replayMatch) {
|
|
9977
|
+
const deviceId = replayMatch[1];
|
|
9978
|
+
const channel = replayMatch[2] ?? "0";
|
|
9979
|
+
return `replay:${deviceId}:ch${channel}`;
|
|
9980
|
+
}
|
|
9981
|
+
const liveMatch = sessionKey.match(/^live:[^:]+:ch(\d+):(\w+)$/);
|
|
9982
|
+
if (liveMatch && liveMatch[1] && liveMatch[2]) {
|
|
9983
|
+
const channel = parseInt(liveMatch[1], 10);
|
|
9984
|
+
const profile = liveMatch[2].toLowerCase();
|
|
9985
|
+
if (profile === "ext") {
|
|
9986
|
+
if (channel === 0) {
|
|
9987
|
+
return "general";
|
|
9988
|
+
}
|
|
9989
|
+
return `streaming:ch${channel}:ext`;
|
|
9990
|
+
}
|
|
9991
|
+
return `streaming:ch${channel}`;
|
|
9992
|
+
}
|
|
9993
|
+
return "general";
|
|
9994
|
+
}
|
|
9995
|
+
/**
|
|
9996
|
+
* Acquire a socket from the pool by tag.
|
|
9997
|
+
* Creates a new socket if needed, or reuses an existing one.
|
|
9998
|
+
*
|
|
9999
|
+
* @param tag - The socket pool tag (from resolveSocketTag)
|
|
10000
|
+
* @param logger - Optional logger for debug output
|
|
10001
|
+
* @returns The socket and a release function
|
|
10002
|
+
*/
|
|
10003
|
+
async acquirePooledSocket(tag, logger) {
|
|
9341
10004
|
const log = logger ?? this.logger;
|
|
9342
|
-
const
|
|
9343
|
-
const
|
|
10005
|
+
const now = Date.now();
|
|
10006
|
+
const cooldownEntry = this.socketPoolCooldowns.get(this.host);
|
|
10007
|
+
if (cooldownEntry) {
|
|
10008
|
+
if (now - cooldownEntry.lastFailureAt > _ReolinkBaichuanApi.SOCKET_POOL_FAILURE_WINDOW_MS) {
|
|
10009
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
10010
|
+
log?.debug?.(
|
|
10011
|
+
`[SocketPool] Cooldown reset for host=${this.host} (no failures for ${_ReolinkBaichuanApi.SOCKET_POOL_FAILURE_WINDOW_MS}ms)`
|
|
10012
|
+
);
|
|
10013
|
+
} else if (now < cooldownEntry.cooldownUntil) {
|
|
10014
|
+
const remainingMs = cooldownEntry.cooldownUntil - now;
|
|
10015
|
+
const error = new Error(
|
|
10016
|
+
`[SocketPool] Host ${this.host} is in cooldown for ${Math.ceil(remainingMs / 1e3)}s due to repeated login failures. tag=${tag}`
|
|
10017
|
+
);
|
|
10018
|
+
log?.warn?.(error.message);
|
|
10019
|
+
throw error;
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
const existing = this.socketPool.get(tag);
|
|
9344
10023
|
if (existing) {
|
|
9345
10024
|
if (existing.idleCloseTimer) {
|
|
9346
10025
|
clearTimeout(existing.idleCloseTimer);
|
|
9347
10026
|
existing.idleCloseTimer = void 0;
|
|
9348
10027
|
}
|
|
9349
|
-
if (existing.
|
|
9350
|
-
|
|
10028
|
+
if (existing.pendingPromise) {
|
|
10029
|
+
const client2 = await existing.pendingPromise;
|
|
10030
|
+
existing.refCount++;
|
|
9351
10031
|
existing.lastUsedAt = Date.now();
|
|
9352
10032
|
log?.debug?.(
|
|
9353
|
-
`[
|
|
10033
|
+
`[SocketPool] Waited for pending socket creation for tag=${tag} (refCount=${existing.refCount})`
|
|
9354
10034
|
);
|
|
10035
|
+
return {
|
|
10036
|
+
client: client2,
|
|
10037
|
+
release: () => this.releasePooledSocket(tag, logger)
|
|
10038
|
+
};
|
|
10039
|
+
}
|
|
10040
|
+
if (existing.refCount === 0) {
|
|
10041
|
+
existing.refCount = 1;
|
|
10042
|
+
existing.lastUsedAt = Date.now();
|
|
10043
|
+
log?.debug?.(`[SocketPool] Reusing idle socket for tag=${tag}`);
|
|
9355
10044
|
try {
|
|
9356
10045
|
if (!existing.client.loggedIn) {
|
|
9357
10046
|
await existing.client.login();
|
|
@@ -9361,132 +10050,223 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9361
10050
|
if (existing.client.loggedIn) {
|
|
9362
10051
|
return {
|
|
9363
10052
|
client: existing.client,
|
|
9364
|
-
release: () => this.
|
|
10053
|
+
release: () => this.releasePooledSocket(tag, logger)
|
|
10054
|
+
};
|
|
10055
|
+
}
|
|
10056
|
+
} else {
|
|
10057
|
+
if (tag.startsWith("replay:")) {
|
|
10058
|
+
log?.debug?.(
|
|
10059
|
+
`[SocketPool] Preempting active replay socket for tag=${tag}`
|
|
10060
|
+
);
|
|
10061
|
+
} else {
|
|
10062
|
+
existing.refCount++;
|
|
10063
|
+
existing.lastUsedAt = Date.now();
|
|
10064
|
+
log?.debug?.(
|
|
10065
|
+
`[SocketPool] Reusing active socket for tag=${tag} (refCount=${existing.refCount})`
|
|
10066
|
+
);
|
|
10067
|
+
return {
|
|
10068
|
+
client: existing.client,
|
|
10069
|
+
release: () => this.releasePooledSocket(tag, logger)
|
|
9365
10070
|
};
|
|
9366
10071
|
}
|
|
9367
10072
|
}
|
|
9368
|
-
log?.
|
|
9369
|
-
`[
|
|
10073
|
+
log?.debug?.(
|
|
10074
|
+
`[SocketPool] Closing existing socket for tag=${tag} (recreating)`
|
|
9370
10075
|
);
|
|
9371
|
-
this.
|
|
10076
|
+
this.socketPool.delete(tag);
|
|
9372
10077
|
try {
|
|
9373
|
-
await existing.client.close({
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
);
|
|
10078
|
+
await existing.client.close({
|
|
10079
|
+
reason: "socket pool recreation",
|
|
10080
|
+
skipLogout: true
|
|
10081
|
+
});
|
|
9377
10082
|
} catch (e) {
|
|
9378
10083
|
log?.warn?.(
|
|
9379
|
-
`[
|
|
10084
|
+
`[SocketPool] Error closing old socket for tag=${tag}: ${e}`
|
|
9380
10085
|
);
|
|
9381
10086
|
}
|
|
9382
10087
|
}
|
|
9383
|
-
log?.log?.(
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
logger: log,
|
|
9391
|
-
debugOptions: this.client.getDebugConfig?.()
|
|
9392
|
-
});
|
|
9393
|
-
await dedicatedClient.login();
|
|
9394
|
-
log?.log?.(
|
|
9395
|
-
`[DedicatedClient] Dedicated socket logged in for sessionKey=${sessionKey}`
|
|
9396
|
-
);
|
|
9397
|
-
this.dedicatedClients.set(sessionKey, {
|
|
9398
|
-
client: dedicatedClient,
|
|
9399
|
-
refCount: 1,
|
|
9400
|
-
createdAt: Date.now(),
|
|
9401
|
-
lastUsedAt: Date.now(),
|
|
10088
|
+
log?.log?.(`[SocketPool] Creating new socket for tag=${tag}`);
|
|
10089
|
+
const entry = {
|
|
10090
|
+
client: void 0,
|
|
10091
|
+
// Will be set after login
|
|
10092
|
+
refCount: 0,
|
|
10093
|
+
createdAt: now,
|
|
10094
|
+
lastUsedAt: now,
|
|
9402
10095
|
idleCloseTimer: void 0
|
|
9403
|
-
}
|
|
10096
|
+
};
|
|
10097
|
+
entry.pendingPromise = (async () => {
|
|
10098
|
+
try {
|
|
10099
|
+
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
10100
|
+
const newClient = new BaichuanClient(clientOpts);
|
|
10101
|
+
await newClient.login();
|
|
10102
|
+
if (this.socketPoolCooldowns.has(this.host)) {
|
|
10103
|
+
log?.debug?.(
|
|
10104
|
+
`[SocketPool] Clearing cooldown for host=${this.host} after successful login`
|
|
10105
|
+
);
|
|
10106
|
+
this.socketPoolCooldowns.delete(this.host);
|
|
10107
|
+
}
|
|
10108
|
+
entry.client = newClient;
|
|
10109
|
+
entry.refCount = 1;
|
|
10110
|
+
entry.lastUsedAt = Date.now();
|
|
10111
|
+
delete entry.pendingPromise;
|
|
10112
|
+
log?.log?.(`[SocketPool] Socket connected for tag=${tag}`);
|
|
10113
|
+
void this.maybeRebootOnTooManySessions();
|
|
10114
|
+
return newClient;
|
|
10115
|
+
} catch (loginError) {
|
|
10116
|
+
const prevCooldown = this.socketPoolCooldowns.get(this.host);
|
|
10117
|
+
const failureCount = (prevCooldown?.failureCount ?? 0) + 1;
|
|
10118
|
+
const now2 = Date.now();
|
|
10119
|
+
let cooldownUntil = now2;
|
|
10120
|
+
if (failureCount >= _ReolinkBaichuanApi.SOCKET_POOL_FAILURE_THRESHOLD) {
|
|
10121
|
+
const backoffMs = Math.min(
|
|
10122
|
+
_ReolinkBaichuanApi.SOCKET_POOL_BASE_COOLDOWN_MS * Math.pow(
|
|
10123
|
+
2,
|
|
10124
|
+
failureCount - _ReolinkBaichuanApi.SOCKET_POOL_FAILURE_THRESHOLD
|
|
10125
|
+
),
|
|
10126
|
+
_ReolinkBaichuanApi.SOCKET_POOL_MAX_COOLDOWN_MS
|
|
10127
|
+
);
|
|
10128
|
+
cooldownUntil = now2 + backoffMs;
|
|
10129
|
+
log?.warn?.(
|
|
10130
|
+
`[SocketPool] Login failed for host=${this.host} (failure #${failureCount}). Entering cooldown for ${Math.ceil(backoffMs / 1e3)}s. tag=${tag}`
|
|
10131
|
+
);
|
|
10132
|
+
} else {
|
|
10133
|
+
log?.warn?.(
|
|
10134
|
+
`[SocketPool] Login failed for host=${this.host} (failure #${failureCount}/${_ReolinkBaichuanApi.SOCKET_POOL_FAILURE_THRESHOLD} before cooldown). tag=${tag}`
|
|
10135
|
+
);
|
|
10136
|
+
}
|
|
10137
|
+
this.socketPoolCooldowns.set(this.host, {
|
|
10138
|
+
failureCount,
|
|
10139
|
+
lastFailureAt: now2,
|
|
10140
|
+
cooldownUntil
|
|
10141
|
+
});
|
|
10142
|
+
this.socketPool.delete(tag);
|
|
10143
|
+
throw loginError;
|
|
10144
|
+
}
|
|
10145
|
+
})();
|
|
10146
|
+
this.socketPool.set(tag, entry);
|
|
10147
|
+
const client = await entry.pendingPromise;
|
|
9404
10148
|
return {
|
|
9405
|
-
client
|
|
9406
|
-
release: () => this.
|
|
10149
|
+
client,
|
|
10150
|
+
release: () => this.releasePooledSocket(tag, logger)
|
|
9407
10151
|
};
|
|
9408
10152
|
}
|
|
9409
10153
|
/**
|
|
9410
|
-
* Release a
|
|
9411
|
-
*
|
|
10154
|
+
* Release a socket back to the pool.
|
|
10155
|
+
* For shared sockets (general, streaming), just decrements refCount.
|
|
10156
|
+
* For replay sockets, schedules idle close.
|
|
9412
10157
|
*/
|
|
9413
|
-
async
|
|
10158
|
+
async releasePooledSocket(tag, logger) {
|
|
9414
10159
|
const log = logger ?? this.logger;
|
|
9415
|
-
const entry = this.
|
|
10160
|
+
const entry = this.socketPool.get(tag);
|
|
9416
10161
|
if (!entry) return;
|
|
9417
10162
|
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
9418
10163
|
entry.lastUsedAt = Date.now();
|
|
10164
|
+
log?.debug?.(
|
|
10165
|
+
`[SocketPool] Released socket for tag=${tag} (refCount=${entry.refCount})`
|
|
10166
|
+
);
|
|
9419
10167
|
if (entry.refCount > 0) return;
|
|
9420
|
-
const
|
|
9421
|
-
const
|
|
9422
|
-
|
|
10168
|
+
const isReplayTag = tag.startsWith("replay:");
|
|
10169
|
+
const isStreamingTag = tag.startsWith("streaming:");
|
|
10170
|
+
const isGeneralTag = tag === "general";
|
|
10171
|
+
if (isGeneralTag) {
|
|
10172
|
+
return;
|
|
10173
|
+
}
|
|
10174
|
+
if (isStreamingTag) {
|
|
10175
|
+
if (entry.idleCloseTimer) return;
|
|
10176
|
+
entry.idleCloseTimer = setTimeout(async () => {
|
|
10177
|
+
const current = this.socketPool.get(tag);
|
|
10178
|
+
if (!current) return;
|
|
10179
|
+
if (current.refCount > 0) return;
|
|
10180
|
+
this.socketPool.delete(tag);
|
|
10181
|
+
log?.log?.(`[SocketPool] Closing idle streaming socket for tag=${tag}`);
|
|
10182
|
+
try {
|
|
10183
|
+
await current.client.close({
|
|
10184
|
+
reason: "streaming idle close",
|
|
10185
|
+
skipLogout: true
|
|
10186
|
+
});
|
|
10187
|
+
} catch {
|
|
10188
|
+
}
|
|
10189
|
+
}, 5e3);
|
|
10190
|
+
return;
|
|
10191
|
+
}
|
|
10192
|
+
if (isReplayTag) {
|
|
9423
10193
|
if (entry.idleCloseTimer) return;
|
|
9424
10194
|
entry.idleCloseTimer = setTimeout(async () => {
|
|
9425
|
-
const current = this.
|
|
10195
|
+
const current = this.socketPool.get(tag);
|
|
9426
10196
|
if (!current) return;
|
|
9427
10197
|
if (current.refCount > 0) return;
|
|
9428
|
-
this.
|
|
10198
|
+
this.socketPool.delete(tag);
|
|
9429
10199
|
log?.debug?.(
|
|
9430
|
-
`[
|
|
10200
|
+
`[SocketPool] Closing idle replay socket for tag=${tag} (keepalive expired)`
|
|
9431
10201
|
);
|
|
9432
10202
|
try {
|
|
9433
10203
|
await current.client.close({
|
|
9434
|
-
reason: "replay idle keepalive expired"
|
|
10204
|
+
reason: "replay idle keepalive expired",
|
|
10205
|
+
skipLogout: true
|
|
9435
10206
|
});
|
|
9436
10207
|
} catch {
|
|
9437
10208
|
}
|
|
9438
|
-
}, _ReolinkBaichuanApi.
|
|
10209
|
+
}, _ReolinkBaichuanApi.SOCKET_POOL_KEEPALIVE_MS);
|
|
9439
10210
|
return;
|
|
9440
10211
|
}
|
|
9441
|
-
this.
|
|
9442
|
-
log?.log?.(
|
|
9443
|
-
`[DedicatedClient] Closing socket for sessionKey=${sessionKey} (session ended)`
|
|
9444
|
-
);
|
|
10212
|
+
this.socketPool.delete(tag);
|
|
9445
10213
|
try {
|
|
9446
|
-
await entry.client.close({
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
);
|
|
10214
|
+
await entry.client.close({
|
|
10215
|
+
reason: "socket pool release",
|
|
10216
|
+
skipLogout: true
|
|
10217
|
+
});
|
|
10218
|
+
log?.log?.(`[SocketPool] Closed socket for tag=${tag}`);
|
|
9450
10219
|
} catch (e) {
|
|
9451
|
-
log?.warn?.(
|
|
9452
|
-
`[DedicatedClient] Error closing socket for sessionKey=${sessionKey}: ${e}`
|
|
9453
|
-
);
|
|
10220
|
+
log?.warn?.(`[SocketPool] Error closing socket for tag=${tag}: ${e}`);
|
|
9454
10221
|
}
|
|
9455
10222
|
}
|
|
9456
10223
|
/**
|
|
9457
|
-
* Force-close a
|
|
9458
|
-
*
|
|
9459
|
-
* for the same sessionKey. The existing stream will receive an error, release its queue slot,
|
|
9460
|
-
* and the new request can then proceed.
|
|
9461
|
-
*
|
|
9462
|
-
* @param sessionKey - The session key to force-close (e.g., `replay:${deviceId}`)
|
|
9463
|
-
* @param logger - Optional logger
|
|
9464
|
-
* @returns true if a client was closed, false if no client existed
|
|
10224
|
+
* Force-close a socket by tag.
|
|
10225
|
+
* Used to preempt existing connections before acquiring a new one.
|
|
9465
10226
|
*/
|
|
9466
|
-
async
|
|
10227
|
+
async forceClosePooledSocket(tag, logger) {
|
|
9467
10228
|
const log = logger ?? this.logger;
|
|
9468
|
-
const entry = this.
|
|
10229
|
+
const entry = this.socketPool.get(tag);
|
|
9469
10230
|
if (!entry) return false;
|
|
9470
10231
|
if (entry.idleCloseTimer) {
|
|
9471
10232
|
clearTimeout(entry.idleCloseTimer);
|
|
9472
10233
|
entry.idleCloseTimer = void 0;
|
|
9473
10234
|
}
|
|
9474
|
-
log?.
|
|
9475
|
-
|
|
9476
|
-
);
|
|
9477
|
-
this.dedicatedClients.delete(sessionKey);
|
|
10235
|
+
log?.debug?.(`[SocketPool] Force-closing socket for tag=${tag}`);
|
|
10236
|
+
this.socketPool.delete(tag);
|
|
9478
10237
|
try {
|
|
9479
|
-
await entry.client.close({
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
);
|
|
10238
|
+
await entry.client.close({
|
|
10239
|
+
reason: "force closed",
|
|
10240
|
+
skipLogout: true
|
|
10241
|
+
});
|
|
10242
|
+
log?.log?.(`[SocketPool] Force-closed socket for tag=${tag}`);
|
|
9483
10243
|
} catch (e) {
|
|
9484
|
-
log?.warn?.(
|
|
9485
|
-
`[DedicatedClient] Error during force-close for sessionKey=${sessionKey}: ${e}`
|
|
9486
|
-
);
|
|
10244
|
+
log?.warn?.(`[SocketPool] Error during force-close for tag=${tag}: ${e}`);
|
|
9487
10245
|
}
|
|
9488
10246
|
return true;
|
|
9489
10247
|
}
|
|
10248
|
+
/**
|
|
10249
|
+
* Cleanup all sockets in the pool. Called during API close.
|
|
10250
|
+
*/
|
|
10251
|
+
async cleanupSocketPool() {
|
|
10252
|
+
const entries = Array.from(this.socketPool.entries());
|
|
10253
|
+
this.socketPool.clear();
|
|
10254
|
+
await Promise.allSettled(
|
|
10255
|
+
entries.map(async ([tag, entry]) => {
|
|
10256
|
+
try {
|
|
10257
|
+
if (entry.idleCloseTimer) {
|
|
10258
|
+
clearTimeout(entry.idleCloseTimer);
|
|
10259
|
+
}
|
|
10260
|
+
this.logger?.debug?.(`[SocketPool] Cleanup: closing tag=${tag}`);
|
|
10261
|
+
await entry.client.close({ reason: "API cleanup", skipLogout: true });
|
|
10262
|
+
} catch {
|
|
10263
|
+
}
|
|
10264
|
+
})
|
|
10265
|
+
);
|
|
10266
|
+
}
|
|
10267
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10268
|
+
// PUBLIC SESSION API (backward compatible)
|
|
10269
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9490
10270
|
/**
|
|
9491
10271
|
* Create a dedicated Baichuan client session for streaming.
|
|
9492
10272
|
* This is useful for consumers that need isolated socket connections per stream.
|
|
@@ -9495,10 +10275,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9495
10275
|
* @param logger - Optional logger for debug output
|
|
9496
10276
|
* @returns Object with `client` (the dedicated BaichuanClient) and `release` function to call when done
|
|
9497
10277
|
*
|
|
9498
|
-
*
|
|
9499
|
-
*
|
|
9500
|
-
*
|
|
9501
|
-
*
|
|
10278
|
+
* Session keys are automatically mapped to socket pool tags:
|
|
10279
|
+
* - `live:device:ch0:ext` → "general" socket (shared with commands/events)
|
|
10280
|
+
* - `live:device:ch0:main` → "streaming" socket (standalone) or "streaming:ch0" (NVR)
|
|
10281
|
+
* - `live:device:ch0:sub` → "streaming" socket (standalone) or "streaming:ch0" (NVR)
|
|
10282
|
+
* - `replay:device:...` → dedicated per-replay socket
|
|
9502
10283
|
*
|
|
9503
10284
|
* @example
|
|
9504
10285
|
* ```typescript
|
|
@@ -9511,26 +10292,25 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9511
10292
|
* ```
|
|
9512
10293
|
*/
|
|
9513
10294
|
async createDedicatedSession(sessionKey, logger) {
|
|
9514
|
-
|
|
10295
|
+
const tag = this.resolveSocketTag(sessionKey);
|
|
10296
|
+
const log = logger ?? this.logger;
|
|
10297
|
+
log?.debug?.(
|
|
10298
|
+
`[SocketPool] createDedicatedSession sessionKey=${sessionKey} \u2192 tag=${tag}`
|
|
10299
|
+
);
|
|
10300
|
+
return await this.acquirePooledSocket(tag, logger);
|
|
10301
|
+
}
|
|
10302
|
+
/**
|
|
10303
|
+
* @deprecated Use forceClosePooledSocket via createDedicatedSession instead.
|
|
10304
|
+
* Force-close a dedicated client if it exists.
|
|
10305
|
+
*/
|
|
10306
|
+
async forceCloseDedicatedClient(sessionKey, logger) {
|
|
10307
|
+
const tag = this.resolveSocketTag(sessionKey);
|
|
10308
|
+
return await this.forceClosePooledSocket(tag, logger);
|
|
9515
10309
|
}
|
|
9516
10310
|
/**
|
|
9517
|
-
* Cleanup
|
|
10311
|
+
* @deprecated Cleanup handled by cleanupSocketPool now.
|
|
9518
10312
|
*/
|
|
9519
10313
|
async cleanupDedicatedClients() {
|
|
9520
|
-
const entries = Array.from(this.dedicatedClients.entries());
|
|
9521
|
-
this.dedicatedClients.clear();
|
|
9522
|
-
await Promise.allSettled(
|
|
9523
|
-
entries.map(async ([key, entry]) => {
|
|
9524
|
-
try {
|
|
9525
|
-
if (entry.idleCloseTimer) {
|
|
9526
|
-
clearTimeout(entry.idleCloseTimer);
|
|
9527
|
-
}
|
|
9528
|
-
this.logger?.debug?.(`[DedicatedClient] Cleanup: closing ${key}`);
|
|
9529
|
-
await entry.client.close({ reason: "API cleanup" });
|
|
9530
|
-
} catch {
|
|
9531
|
-
}
|
|
9532
|
-
})
|
|
9533
|
-
);
|
|
9534
10314
|
}
|
|
9535
10315
|
dispatchSimpleEvent(evt) {
|
|
9536
10316
|
const debugCfg = this.client.getDebugConfig?.();
|
|
@@ -9568,7 +10348,23 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9568
10348
|
opts.logger,
|
|
9569
10349
|
dbg.general || dbg.traceNativeStream || dbg.traceRecordings || dbg.traceTalk || dbg.traceEvents || dbg.debugRtsp
|
|
9570
10350
|
);
|
|
9571
|
-
this.
|
|
10351
|
+
this.clientOptions = {
|
|
10352
|
+
host: opts.host,
|
|
10353
|
+
username: opts.username,
|
|
10354
|
+
password: opts.password,
|
|
10355
|
+
...opts.logger ? { logger: opts.logger } : {},
|
|
10356
|
+
...opts.debugOptions ? { debugOptions: opts.debugOptions } : {},
|
|
10357
|
+
...opts.uid ? { uid: opts.uid } : {}
|
|
10358
|
+
};
|
|
10359
|
+
const generalClient = new BaichuanClient(opts);
|
|
10360
|
+
this.socketPool.set("general", {
|
|
10361
|
+
client: generalClient,
|
|
10362
|
+
refCount: 1,
|
|
10363
|
+
// Always keep general socket "in use"
|
|
10364
|
+
createdAt: Date.now(),
|
|
10365
|
+
lastUsedAt: Date.now(),
|
|
10366
|
+
idleCloseTimer: void 0
|
|
10367
|
+
});
|
|
9572
10368
|
this.host = opts.host;
|
|
9573
10369
|
this.username = opts.username;
|
|
9574
10370
|
this.password = opts.password;
|
|
@@ -9585,7 +10381,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9585
10381
|
username: opts.username,
|
|
9586
10382
|
password: opts.password,
|
|
9587
10383
|
logger: this.logger,
|
|
9588
|
-
debugConfig:
|
|
10384
|
+
debugConfig: generalClient.getDebugConfig?.()
|
|
9589
10385
|
});
|
|
9590
10386
|
this.client.on("event", (event) => {
|
|
9591
10387
|
const mapped = mapToSimpleEvent(event);
|
|
@@ -9623,9 +10419,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9623
10419
|
);
|
|
9624
10420
|
}
|
|
9625
10421
|
});
|
|
9626
|
-
const
|
|
9627
|
-
if (typeof
|
|
9628
|
-
this.
|
|
10422
|
+
const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
|
|
10423
|
+
if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
|
|
10424
|
+
this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
|
|
10425
|
+
}
|
|
10426
|
+
const disconnectThreshold = opts.rebootAfterDisconnectionsPerMinute;
|
|
10427
|
+
if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
|
|
10428
|
+
this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
|
|
10429
|
+
}
|
|
10430
|
+
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
9629
10431
|
this.client.on("close", () => {
|
|
9630
10432
|
try {
|
|
9631
10433
|
void this.maybeRebootOnDisconnectStorm();
|
|
@@ -9633,6 +10435,24 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9633
10435
|
}
|
|
9634
10436
|
});
|
|
9635
10437
|
}
|
|
10438
|
+
const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
|
|
10439
|
+
if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
|
|
10440
|
+
this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
|
|
10441
|
+
}
|
|
10442
|
+
if (this.rebootAfterConsecutiveEconnreset > 0) {
|
|
10443
|
+
this.client.on("close", () => {
|
|
10444
|
+
try {
|
|
10445
|
+
void this.maybeRebootOnEconnresetStorm();
|
|
10446
|
+
} catch {
|
|
10447
|
+
}
|
|
10448
|
+
});
|
|
10449
|
+
}
|
|
10450
|
+
this.client.once("push", () => {
|
|
10451
|
+
void this.logActiveSessionsOnStartup();
|
|
10452
|
+
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
10453
|
+
void this.maybeRebootOnTooManySessions();
|
|
10454
|
+
}, 6e4);
|
|
10455
|
+
});
|
|
9636
10456
|
}
|
|
9637
10457
|
/**
|
|
9638
10458
|
* CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
|
|
@@ -9720,24 +10540,228 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9720
10540
|
});
|
|
9721
10541
|
}
|
|
9722
10542
|
/**
|
|
9723
|
-
* Multifocal/NVR empirical stream diagnostics:
|
|
9724
|
-
* probes RTSP/RTMP candidates + native streams, prints discovered resolutions,
|
|
9725
|
-
* and saves one clip per working stream into a timestamped folder under outDir.
|
|
10543
|
+
* Multifocal/NVR empirical stream diagnostics:
|
|
10544
|
+
* probes RTSP/RTMP candidates + native streams, prints discovered resolutions,
|
|
10545
|
+
* and saves one clip per working stream into a timestamped folder under outDir.
|
|
10546
|
+
*/
|
|
10547
|
+
async runMultifocalDiagnosticsConsecutively(params) {
|
|
10548
|
+
return await runMultifocalDiagnosticsConsecutively({
|
|
10549
|
+
...params,
|
|
10550
|
+
api: this,
|
|
10551
|
+
logger: params.logger ?? this.logger,
|
|
10552
|
+
host: this.host,
|
|
10553
|
+
username: this.username,
|
|
10554
|
+
password: this.password
|
|
10555
|
+
});
|
|
10556
|
+
}
|
|
10557
|
+
/**
|
|
10558
|
+
* Log active sessions on the device at startup for debugging purposes.
|
|
10559
|
+
*/
|
|
10560
|
+
async logActiveSessionsOnStartup() {
|
|
10561
|
+
const localSessionCount = this.socketPool.size;
|
|
10562
|
+
try {
|
|
10563
|
+
const ourIp = this.client.getLocalAddress?.();
|
|
10564
|
+
const onlineUsers = await this.getOnlineUserList({ timeoutMs: 5e3 });
|
|
10565
|
+
const rawLegacy = onlineUsers.body?.OnlineUserList?.item;
|
|
10566
|
+
const rawCurrent = onlineUsers.body?.OnlineUserList?.OnlineUser;
|
|
10567
|
+
const legacyItems = Array.isArray(rawLegacy) ? rawLegacy : rawLegacy ? [rawLegacy] : [];
|
|
10568
|
+
const currentItems = Array.isArray(rawCurrent) ? rawCurrent : rawCurrent ? [rawCurrent] : [];
|
|
10569
|
+
const allItems = currentItems.length > 0 ? currentItems.map((u) => ({
|
|
10570
|
+
ip: u.ipAddress,
|
|
10571
|
+
userName: u.userName,
|
|
10572
|
+
level: u.userLevel,
|
|
10573
|
+
sessionId: u.sessionId
|
|
10574
|
+
})) : legacyItems.map((u) => ({
|
|
10575
|
+
ip: u.ip,
|
|
10576
|
+
userName: u.userName,
|
|
10577
|
+
level: u.level,
|
|
10578
|
+
sessionId: void 0
|
|
10579
|
+
}));
|
|
10580
|
+
const ourSessions = ourIp ? allItems.filter((item) => item.ip === ourIp) : allItems;
|
|
10581
|
+
const deviceReportsZero = allItems.length === 0 && localSessionCount > 0;
|
|
10582
|
+
this.logger.log?.(
|
|
10583
|
+
`[ReolinkBaichuanApi] Startup session check: ${ourSessions.length} device session(s) from our IP (${ourIp ?? "unknown"}), ${allItems.length} total on device, ${localSessionCount} local${deviceReportsZero ? " [device may not support OnlineUserList]" : ""}`,
|
|
10584
|
+
{
|
|
10585
|
+
host: this.host,
|
|
10586
|
+
ourIp,
|
|
10587
|
+
localSessionCount,
|
|
10588
|
+
deviceReportsZero,
|
|
10589
|
+
ourSessions: ourSessions.map((s) => ({
|
|
10590
|
+
user: s.userName,
|
|
10591
|
+
ip: s.ip,
|
|
10592
|
+
sessionId: s.sessionId
|
|
10593
|
+
})),
|
|
10594
|
+
allSessions: allItems.map((s) => ({
|
|
10595
|
+
user: s.userName,
|
|
10596
|
+
ip: s.ip,
|
|
10597
|
+
sessionId: s.sessionId
|
|
10598
|
+
}))
|
|
10599
|
+
}
|
|
10600
|
+
);
|
|
10601
|
+
} catch (e) {
|
|
10602
|
+
this.logger.debug?.(
|
|
10603
|
+
"[ReolinkBaichuanApi] Could not query sessions at startup",
|
|
10604
|
+
e
|
|
10605
|
+
);
|
|
10606
|
+
}
|
|
10607
|
+
}
|
|
10608
|
+
/**
|
|
10609
|
+
* Check if too many sessions are open on the device and trigger a reboot if needed.
|
|
10610
|
+
* Called when acquiring a new dedicated client.
|
|
10611
|
+
* Uses the native Baichuan API to get the actual session count from the device.
|
|
10612
|
+
* Only counts sessions from our own IP address.
|
|
10613
|
+
*/
|
|
10614
|
+
async maybeRebootOnTooManySessions() {
|
|
10615
|
+
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
10616
|
+
if (this.sessionGuardRebootInFlight) return;
|
|
10617
|
+
const cooldownMs = 10 * 6e4;
|
|
10618
|
+
const now = Date.now();
|
|
10619
|
+
if (this.sessionGuardLastRebootAtMs != null && now - this.sessionGuardLastRebootAtMs < cooldownMs) {
|
|
10620
|
+
return;
|
|
10621
|
+
}
|
|
10622
|
+
const ourIp = this.client.getLocalAddress?.();
|
|
10623
|
+
const ourDedicatedSessions = this.socketPool.size;
|
|
10624
|
+
let sessionCount;
|
|
10625
|
+
let sessionItems = [];
|
|
10626
|
+
let ourSessionCount = 0;
|
|
10627
|
+
let usedLocalFallback = false;
|
|
10628
|
+
try {
|
|
10629
|
+
const onlineUsers = await this.getOnlineUserList({ timeoutMs: 5e3 });
|
|
10630
|
+
const rawLegacy = onlineUsers.body?.OnlineUserList?.item;
|
|
10631
|
+
const rawCurrent = onlineUsers.body?.OnlineUserList?.OnlineUser;
|
|
10632
|
+
const legacyItems = Array.isArray(rawLegacy) ? rawLegacy : rawLegacy ? [rawLegacy] : [];
|
|
10633
|
+
const currentItems = Array.isArray(rawCurrent) ? rawCurrent : rawCurrent ? [rawCurrent] : [];
|
|
10634
|
+
const allItems = currentItems.length > 0 ? currentItems.map((u) => ({
|
|
10635
|
+
ip: u.ipAddress,
|
|
10636
|
+
name: u.userName,
|
|
10637
|
+
level: u.userLevel,
|
|
10638
|
+
sessionId: u.sessionId
|
|
10639
|
+
})) : legacyItems.map((u) => ({
|
|
10640
|
+
ip: u.ip,
|
|
10641
|
+
name: u.userName,
|
|
10642
|
+
level: u.level,
|
|
10643
|
+
sessionId: u.sessionId
|
|
10644
|
+
}));
|
|
10645
|
+
sessionItems = allItems;
|
|
10646
|
+
this.logger.debug?.(
|
|
10647
|
+
`[ReolinkBaichuanApi] Session guard: camera reports ${allItems.length} sessions, ourIp=${ourIp ?? "unknown"}`,
|
|
10648
|
+
{
|
|
10649
|
+
ourIp,
|
|
10650
|
+
sessions: allItems
|
|
10651
|
+
}
|
|
10652
|
+
);
|
|
10653
|
+
if (ourIp) {
|
|
10654
|
+
const ourSessions = allItems.filter((item) => item.ip === ourIp);
|
|
10655
|
+
ourSessionCount = ourSessions.length;
|
|
10656
|
+
sessionCount = ourSessionCount;
|
|
10657
|
+
const currentSessionIds = new Set(
|
|
10658
|
+
ourSessions.map((s) => s.sessionId).filter((id) => id != null)
|
|
10659
|
+
);
|
|
10660
|
+
if (this.lastKnownSessionCount !== void 0) {
|
|
10661
|
+
const newIds = [...currentSessionIds].filter(
|
|
10662
|
+
(id) => !this.lastKnownSessionIds.has(id)
|
|
10663
|
+
);
|
|
10664
|
+
const removedIds = [...this.lastKnownSessionIds].filter(
|
|
10665
|
+
(id) => !currentSessionIds.has(id)
|
|
10666
|
+
);
|
|
10667
|
+
if (newIds.length > 0 || removedIds.length > 0) {
|
|
10668
|
+
this.logger.log?.(
|
|
10669
|
+
`[ReolinkBaichuanApi] Session change detected: ${this.lastKnownSessionCount} -> ${ourSessionCount} sessions`,
|
|
10670
|
+
{
|
|
10671
|
+
newSessionIds: newIds,
|
|
10672
|
+
removedSessionIds: removedIds,
|
|
10673
|
+
currentSessions: ourSessions.map((s) => ({
|
|
10674
|
+
id: s.sessionId,
|
|
10675
|
+
ip: s.ip
|
|
10676
|
+
})),
|
|
10677
|
+
localDedicatedSessions: Array.from(this.socketPool.keys())
|
|
10678
|
+
}
|
|
10679
|
+
);
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
10682
|
+
this.lastKnownSessionCount = ourSessionCount;
|
|
10683
|
+
this.lastKnownSessionIds = currentSessionIds;
|
|
10684
|
+
if (allItems.length > 0) {
|
|
10685
|
+
this.logger.debug?.(
|
|
10686
|
+
`[ReolinkBaichuanApi] Session guard: ${ourSessionCount}/${allItems.length} sessions match ourIp=${ourIp}`
|
|
10687
|
+
);
|
|
10688
|
+
}
|
|
10689
|
+
} else {
|
|
10690
|
+
sessionCount = onlineUsers.body?.OnlineUserList?.itemNum ?? 0;
|
|
10691
|
+
ourSessionCount = sessionCount;
|
|
10692
|
+
}
|
|
10693
|
+
if (sessionCount === 0 && ourDedicatedSessions > 0) {
|
|
10694
|
+
sessionCount = ourDedicatedSessions;
|
|
10695
|
+
ourSessionCount = ourDedicatedSessions;
|
|
10696
|
+
usedLocalFallback = true;
|
|
10697
|
+
this.logger.debug?.(
|
|
10698
|
+
`[ReolinkBaichuanApi] Session guard: camera reports 0 sessions but we have ${ourDedicatedSessions} local dedicated sessions, using local fallback`
|
|
10699
|
+
);
|
|
10700
|
+
}
|
|
10701
|
+
} catch (e) {
|
|
10702
|
+
this.logger.debug?.(
|
|
10703
|
+
"[ReolinkBaichuanApi] Session guard: failed to query online users, using local count",
|
|
10704
|
+
e
|
|
10705
|
+
);
|
|
10706
|
+
sessionCount = this.socketPool.size;
|
|
10707
|
+
ourSessionCount = sessionCount;
|
|
10708
|
+
usedLocalFallback = true;
|
|
10709
|
+
}
|
|
10710
|
+
if (sessionCount < threshold) return;
|
|
10711
|
+
this.sessionGuardLastRebootAtMs = now;
|
|
10712
|
+
const localSessions = Array.from(this.socketPool.keys());
|
|
10713
|
+
const thresholdIsDefault = this.maxDedicatedSessionsBeforeReboot == null;
|
|
10714
|
+
(this.logger.warn ?? this.logger.log).call(
|
|
10715
|
+
this.logger,
|
|
10716
|
+
`[ReolinkBaichuanApi] Too many sessions from our IP (${ourIp ?? "unknown"}) on device host=${this.host} (${sessionCount} >= ${threshold}${thresholdIsDefault ? " [default]" : ""}${usedLocalFallback ? " [local fallback]" : ""}); rebooting device`,
|
|
10717
|
+
{
|
|
10718
|
+
host: this.host,
|
|
10719
|
+
ourIp,
|
|
10720
|
+
ourSessionCount,
|
|
10721
|
+
threshold,
|
|
10722
|
+
usedLocalFallback,
|
|
10723
|
+
thresholdFormula: thresholdIsDefault ? "default (10)" : "explicit",
|
|
10724
|
+
localDedicatedSessions: localSessions,
|
|
10725
|
+
allDeviceSessions: sessionItems
|
|
10726
|
+
}
|
|
10727
|
+
);
|
|
10728
|
+
this.sessionGuardRebootInFlight = this.rebootFromSessionGuard().catch((e) => {
|
|
10729
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
10730
|
+
this.logger,
|
|
10731
|
+
"[ReolinkBaichuanApi] Session guard reboot failed",
|
|
10732
|
+
e
|
|
10733
|
+
);
|
|
10734
|
+
}).finally(() => {
|
|
10735
|
+
this.sessionGuardRebootInFlight = void 0;
|
|
10736
|
+
});
|
|
10737
|
+
}
|
|
10738
|
+
async rebootFromSessionGuard() {
|
|
10739
|
+
try {
|
|
10740
|
+
await this.reboot();
|
|
10741
|
+
return;
|
|
10742
|
+
} catch (e) {
|
|
10743
|
+
this.logger.debug?.(
|
|
10744
|
+
"[ReolinkBaichuanApi] Baichuan reboot failed, trying CGI",
|
|
10745
|
+
e
|
|
10746
|
+
);
|
|
10747
|
+
}
|
|
10748
|
+
try {
|
|
10749
|
+
await this.cgiApi.login();
|
|
10750
|
+
await this.cgiApi.Reboot();
|
|
10751
|
+
} catch (e) {
|
|
10752
|
+
throw e instanceof Error ? e : new Error(String(e ?? "session guard reboot failed"));
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
/**
|
|
10756
|
+
* Check if there are too many voluntary disconnections and trigger a reboot if needed.
|
|
10757
|
+
* Called on every socket close event.
|
|
9726
10758
|
*/
|
|
9727
|
-
async runMultifocalDiagnosticsConsecutively(params) {
|
|
9728
|
-
return await runMultifocalDiagnosticsConsecutively({
|
|
9729
|
-
...params,
|
|
9730
|
-
api: this,
|
|
9731
|
-
logger: params.logger ?? this.logger,
|
|
9732
|
-
host: this.host,
|
|
9733
|
-
username: this.username,
|
|
9734
|
-
password: this.password
|
|
9735
|
-
});
|
|
9736
|
-
}
|
|
9737
10759
|
async maybeRebootOnDisconnectStorm() {
|
|
9738
10760
|
const threshold = this.rebootAfterDisconnectionsPerMinute;
|
|
9739
|
-
if (threshold
|
|
9740
|
-
const
|
|
10761
|
+
if (threshold <= 0) return;
|
|
10762
|
+
const entry = this.socketPool.get("general");
|
|
10763
|
+
if (!entry) return;
|
|
10764
|
+
const info = entry.client.getLastDisconnectInfo?.();
|
|
9741
10765
|
if (!info?.voluntary) return;
|
|
9742
10766
|
const now = Date.now();
|
|
9743
10767
|
const windowMs = 6e4;
|
|
@@ -9749,54 +10773,90 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9749
10773
|
if (this.disconnectStormVoluntaryAtMs.length < threshold) return;
|
|
9750
10774
|
if (this.disconnectStormRebootInFlight) return;
|
|
9751
10775
|
const cooldownMs = 10 * 6e4;
|
|
9752
|
-
if (this.disconnectStormLastRebootAtMs != null && now - this.disconnectStormLastRebootAtMs < cooldownMs)
|
|
10776
|
+
if (this.disconnectStormLastRebootAtMs != null && now - this.disconnectStormLastRebootAtMs < cooldownMs) {
|
|
9753
10777
|
return;
|
|
10778
|
+
}
|
|
9754
10779
|
this.disconnectStormLastRebootAtMs = now;
|
|
9755
|
-
(this.logger.warn ?? this.logger.
|
|
10780
|
+
(this.logger.warn ?? this.logger.log).call(
|
|
9756
10781
|
this.logger,
|
|
9757
|
-
|
|
10782
|
+
`[ReolinkBaichuanApi] Disconnect storm detected for host=${this.host} (${this.disconnectStormVoluntaryAtMs.length} voluntary disconnects in 60s >= ${threshold}); rebooting device`,
|
|
9758
10783
|
{
|
|
10784
|
+
host: this.host,
|
|
9759
10785
|
transport: info.transport,
|
|
9760
10786
|
reason: info.reason,
|
|
9761
10787
|
voluntaryDisconnectsInWindow: this.disconnectStormVoluntaryAtMs.length,
|
|
9762
|
-
windowMs,
|
|
9763
10788
|
threshold,
|
|
9764
|
-
|
|
9765
|
-
|
|
10789
|
+
windowMs,
|
|
10790
|
+
cooldownMs
|
|
9766
10791
|
}
|
|
9767
10792
|
);
|
|
9768
|
-
this.disconnectStormRebootInFlight = this.
|
|
10793
|
+
this.disconnectStormRebootInFlight = this.rebootFromSessionGuard().catch((e) => {
|
|
9769
10794
|
(this.logger.warn ?? this.logger.error).call(
|
|
9770
10795
|
this.logger,
|
|
9771
|
-
"[ReolinkBaichuanApi]
|
|
10796
|
+
"[ReolinkBaichuanApi] Disconnect storm reboot failed",
|
|
9772
10797
|
e
|
|
9773
10798
|
);
|
|
9774
10799
|
}).finally(() => {
|
|
9775
10800
|
this.disconnectStormRebootInFlight = void 0;
|
|
9776
10801
|
});
|
|
9777
10802
|
}
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
10803
|
+
/**
|
|
10804
|
+
* Check if there are too many consecutive ECONNRESET errors and trigger a reboot if needed.
|
|
10805
|
+
* This guards against camera saturation where the device refuses all new connections.
|
|
10806
|
+
* Called on every socket close event.
|
|
10807
|
+
*/
|
|
10808
|
+
async maybeRebootOnEconnresetStorm() {
|
|
10809
|
+
const threshold = this.rebootAfterConsecutiveEconnreset;
|
|
10810
|
+
if (threshold <= 0) return;
|
|
10811
|
+
const entry = this.socketPool.get("general");
|
|
10812
|
+
if (!entry) return;
|
|
10813
|
+
const info = entry.client.getLastDisconnectInfo?.();
|
|
10814
|
+
const isEconnreset = info?.errorCode === "ECONNRESET";
|
|
10815
|
+
if (!isEconnreset) {
|
|
10816
|
+
this.consecutiveEconnresetCount = 0;
|
|
10817
|
+
this.consecutiveEconnresetFirstAtMs = void 0;
|
|
10818
|
+
return;
|
|
9788
10819
|
}
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
return;
|
|
9794
|
-
} catch (e) {
|
|
9795
|
-
lastErr = e;
|
|
9796
|
-
if (method === "cgi") throw e;
|
|
9797
|
-
}
|
|
10820
|
+
const now = Date.now();
|
|
10821
|
+
const windowMs = 6e4;
|
|
10822
|
+
if (this.consecutiveEconnresetFirstAtMs == null) {
|
|
10823
|
+
this.consecutiveEconnresetFirstAtMs = now;
|
|
9798
10824
|
}
|
|
9799
|
-
|
|
10825
|
+
if (now - this.consecutiveEconnresetFirstAtMs > windowMs) {
|
|
10826
|
+
this.consecutiveEconnresetCount = 1;
|
|
10827
|
+
this.consecutiveEconnresetFirstAtMs = now;
|
|
10828
|
+
} else {
|
|
10829
|
+
this.consecutiveEconnresetCount++;
|
|
10830
|
+
}
|
|
10831
|
+
if (this.consecutiveEconnresetCount < threshold) return;
|
|
10832
|
+
if (this.econnresetStormRebootInFlight) return;
|
|
10833
|
+
const cooldownMs = 10 * 6e4;
|
|
10834
|
+
if (this.econnresetStormLastRebootAtMs != null && now - this.econnresetStormLastRebootAtMs < cooldownMs) {
|
|
10835
|
+
return;
|
|
10836
|
+
}
|
|
10837
|
+
this.econnresetStormLastRebootAtMs = now;
|
|
10838
|
+
(this.logger.warn ?? this.logger.log).call(
|
|
10839
|
+
this.logger,
|
|
10840
|
+
`[ReolinkBaichuanApi] ECONNRESET storm detected for host=${this.host} (${this.consecutiveEconnresetCount} consecutive ECONNRESET in 60s >= ${threshold}); rebooting device`,
|
|
10841
|
+
{
|
|
10842
|
+
host: this.host,
|
|
10843
|
+
consecutiveEconnreset: this.consecutiveEconnresetCount,
|
|
10844
|
+
threshold,
|
|
10845
|
+
windowMs,
|
|
10846
|
+
cooldownMs
|
|
10847
|
+
}
|
|
10848
|
+
);
|
|
10849
|
+
this.consecutiveEconnresetCount = 0;
|
|
10850
|
+
this.consecutiveEconnresetFirstAtMs = void 0;
|
|
10851
|
+
this.econnresetStormRebootInFlight = this.rebootFromSessionGuard().catch((e) => {
|
|
10852
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
10853
|
+
this.logger,
|
|
10854
|
+
"[ReolinkBaichuanApi] ECONNRESET storm reboot failed",
|
|
10855
|
+
e
|
|
10856
|
+
);
|
|
10857
|
+
}).finally(() => {
|
|
10858
|
+
this.econnresetStormRebootInFlight = void 0;
|
|
10859
|
+
});
|
|
9800
10860
|
}
|
|
9801
10861
|
/**
|
|
9802
10862
|
* Subscribe to minimal high-level events.
|
|
@@ -9941,12 +11001,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9941
11001
|
/**
|
|
9942
11002
|
* Determines if this device is an NVR/Hub (multiple channels) vs a standalone camera.
|
|
9943
11003
|
* Checks:
|
|
9944
|
-
* 1.
|
|
9945
|
-
* 2.
|
|
11004
|
+
* 1. Cached value from setIsNvr()
|
|
11005
|
+
* 2. Channel count > 3 (typical NVR detection)
|
|
11006
|
+
* 3. Device model matches NVR/Hub patterns (for devices like Home Hub that report channelNum=1)
|
|
9946
11007
|
*/
|
|
9947
11008
|
async isNvrDevice() {
|
|
11009
|
+
if (this._isNvr !== void 0) {
|
|
11010
|
+
return this._isNvr;
|
|
11011
|
+
}
|
|
9948
11012
|
const channelCount = await this.getChannelCount();
|
|
9949
|
-
if (channelCount > 3)
|
|
11013
|
+
if (channelCount > 3) {
|
|
11014
|
+
this._isNvr = true;
|
|
11015
|
+
return true;
|
|
11016
|
+
}
|
|
9950
11017
|
try {
|
|
9951
11018
|
const info = await this.getInfo(void 0, {
|
|
9952
11019
|
tags: ["type"],
|
|
@@ -9956,23 +11023,76 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9956
11023
|
this.logger.debug?.(
|
|
9957
11024
|
`[ReolinkBaichuanApi] isNvrDevice: type="${info.type}" matches NVR/Hub pattern`
|
|
9958
11025
|
);
|
|
11026
|
+
this._isNvr = true;
|
|
9959
11027
|
return true;
|
|
9960
11028
|
}
|
|
9961
11029
|
} catch {
|
|
9962
11030
|
}
|
|
11031
|
+
this._isNvr = false;
|
|
9963
11032
|
return false;
|
|
9964
11033
|
}
|
|
11034
|
+
/**
|
|
11035
|
+
* Set the NVR/Hub flag explicitly.
|
|
11036
|
+
* Call this early (before streaming) to ensure correct socket pooling.
|
|
11037
|
+
* @param isNvr - true if this is an NVR/Hub, false for standalone camera
|
|
11038
|
+
*/
|
|
11039
|
+
setIsNvr(isNvr) {
|
|
11040
|
+
this._isNvr = isNvr;
|
|
11041
|
+
this.logger.debug?.(`[ReolinkBaichuanApi] setIsNvr: ${isNvr}`);
|
|
11042
|
+
}
|
|
11043
|
+
/**
|
|
11044
|
+
* Enable or disable idle disconnect dynamically.
|
|
11045
|
+
*
|
|
11046
|
+
* This is useful when the battery status is discovered after connection
|
|
11047
|
+
* (e.g., during autodetect). Call with `true` for battery cameras to
|
|
11048
|
+
* preserve battery life by disconnecting when idle.
|
|
11049
|
+
*
|
|
11050
|
+
* @param enabled - true to enable idle disconnect, false to disable
|
|
11051
|
+
*/
|
|
11052
|
+
setIdleDisconnect(enabled) {
|
|
11053
|
+
this.client.setIdleDisconnect(enabled);
|
|
11054
|
+
this.logger.debug?.(`[ReolinkBaichuanApi] setIdleDisconnect: ${enabled}`);
|
|
11055
|
+
}
|
|
9965
11056
|
async login(maxEncryption) {
|
|
9966
11057
|
await this.client.login(maxEncryption);
|
|
9967
11058
|
}
|
|
11059
|
+
/**
|
|
11060
|
+
* Stop all active video streams on the main API client.
|
|
11061
|
+
* Called automatically during close() to ensure clean session termination.
|
|
11062
|
+
*/
|
|
11063
|
+
async stopAllActiveStreams() {
|
|
11064
|
+
const activeStreams = Array.from(this.activeVideoMsgNums.keys());
|
|
11065
|
+
if (activeStreams.length === 0) {
|
|
11066
|
+
return;
|
|
11067
|
+
}
|
|
11068
|
+
this.logger?.debug?.(
|
|
11069
|
+
`[ReolinkBaichuanApi] Stopping ${activeStreams.length} active stream(s) before close`
|
|
11070
|
+
);
|
|
11071
|
+
await Promise.allSettled(
|
|
11072
|
+
activeStreams.map(async (key) => {
|
|
11073
|
+
const [ch, profile, variant] = key.split(":");
|
|
11074
|
+
try {
|
|
11075
|
+
await this.stopVideoStream(Number(ch), profile, {
|
|
11076
|
+
variant
|
|
11077
|
+
});
|
|
11078
|
+
} catch (e) {
|
|
11079
|
+
this.logger?.debug?.(
|
|
11080
|
+
`[ReolinkBaichuanApi] Error stopping stream ${key}: ${e instanceof Error ? e.message : String(e)}`
|
|
11081
|
+
);
|
|
11082
|
+
}
|
|
11083
|
+
})
|
|
11084
|
+
);
|
|
11085
|
+
}
|
|
9968
11086
|
async close(options) {
|
|
11087
|
+
if (this.sessionGuardIntervalTimer) {
|
|
11088
|
+
clearInterval(this.sessionGuardIntervalTimer);
|
|
11089
|
+
this.sessionGuardIntervalTimer = void 0;
|
|
11090
|
+
}
|
|
9969
11091
|
this.stopStatePolling();
|
|
9970
11092
|
this.stopUdpSleepInference();
|
|
9971
11093
|
await this.cleanup();
|
|
9972
|
-
await this.
|
|
9973
|
-
await this.
|
|
9974
|
-
options?.reason ? { reason: options.reason } : void 0
|
|
9975
|
-
);
|
|
11094
|
+
await this.stopAllActiveStreams();
|
|
11095
|
+
await this.cleanupSocketPool();
|
|
9976
11096
|
}
|
|
9977
11097
|
/**
|
|
9978
11098
|
* Cleanup all RTSP servers and release resources.
|
|
@@ -10175,10 +11295,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10175
11295
|
logger?.info?.(
|
|
10176
11296
|
`[DedicatedTalk] Creating session: ${sessionKey} (idleTimeout=${idleTimeoutMs}ms)`
|
|
10177
11297
|
);
|
|
10178
|
-
const
|
|
10179
|
-
const
|
|
11298
|
+
const tag = this.resolveSocketTag(sessionKey);
|
|
11299
|
+
const { client: dedicatedClient, release } = await this.acquirePooledSocket(
|
|
11300
|
+
tag,
|
|
11301
|
+
logger
|
|
11302
|
+
);
|
|
11303
|
+
const summary = this.getSocketPoolSummary();
|
|
10180
11304
|
logger?.info?.(
|
|
10181
|
-
`[DedicatedTalk] Session created [sessions: ${summary.count} active${summary.count > 0 ? ` (${summary.
|
|
11305
|
+
`[DedicatedTalk] Session created [sessions: ${summary.count} active${summary.count > 0 ? ` (${summary.tags.join(", ")})` : ""}]`
|
|
10182
11306
|
);
|
|
10183
11307
|
try {
|
|
10184
11308
|
const isUdp = dedicatedClient.getTransport?.() === "udp";
|
|
@@ -11301,8 +12425,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11301
12425
|
const channel = params.channel;
|
|
11302
12426
|
const streamType = params.streamType;
|
|
11303
12427
|
const logger = params.logger ?? this.logger;
|
|
11304
|
-
const sessionKey = params.deviceId ? `replay:${params.deviceId}` : `replay:standalone
|
|
11305
|
-
const
|
|
12428
|
+
const sessionKey = params.deviceId ? `replay:${params.deviceId}:ch${channel}` : `replay:standalone:ch${channel}:${Date.now()}`;
|
|
12429
|
+
const tag = this.resolveSocketTag(sessionKey);
|
|
12430
|
+
logger?.debug?.(
|
|
12431
|
+
`[startRecordingReplayStreamStandalone] sessionKey=${sessionKey} -> tag=${tag}`
|
|
12432
|
+
);
|
|
12433
|
+
const { client: dedicatedClient, release: releaseDedicatedClient } = await this.acquirePooledSocket(tag, logger);
|
|
11306
12434
|
const uid = await this.ensureUidForRecordings(channel, void 0);
|
|
11307
12435
|
const payloadXml = buildFileInfoListReplayByNameXml({
|
|
11308
12436
|
channel,
|
|
@@ -11430,8 +12558,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11430
12558
|
const channel = params.channel;
|
|
11431
12559
|
const streamType = params.streamType;
|
|
11432
12560
|
const logger = params.logger ?? this.logger;
|
|
11433
|
-
const sessionKey = params.deviceId ? `replay:${params.deviceId}` : `replay:nvr
|
|
11434
|
-
const
|
|
12561
|
+
const sessionKey = params.deviceId ? `replay:${params.deviceId}:ch${channel}` : `replay:nvr:ch${channel}:${Date.now()}`;
|
|
12562
|
+
const tag = this.resolveSocketTag(sessionKey);
|
|
12563
|
+
const { client: dedicatedClient, release: releaseDedicatedClient } = await this.acquirePooledSocket(tag, logger);
|
|
11435
12564
|
let uid;
|
|
11436
12565
|
try {
|
|
11437
12566
|
uid = await this.ensureUidForRecordings(channel, void 0);
|
|
@@ -13175,6 +14304,9 @@ ${stderr}`)
|
|
|
13175
14304
|
* After subscribing, events will be emitted via client.on("event", ...)
|
|
13176
14305
|
*/
|
|
13177
14306
|
async subscribeEvents() {
|
|
14307
|
+
this.logger.debug?.(
|
|
14308
|
+
"[ReolinkBaichuanApi] subscribeEvents() called - checking session count before"
|
|
14309
|
+
);
|
|
13178
14310
|
await this.client.login();
|
|
13179
14311
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
13180
14312
|
const attempts = [
|
|
@@ -15448,7 +16580,8 @@ ${xml}`
|
|
|
15448
16580
|
const result2 = {
|
|
15449
16581
|
nativeStreams,
|
|
15450
16582
|
rtmpStreams,
|
|
15451
|
-
rtspStreams
|
|
16583
|
+
rtspStreams,
|
|
16584
|
+
rawEncXml: void 0
|
|
15452
16585
|
};
|
|
15453
16586
|
return cacheOrFallback(result2);
|
|
15454
16587
|
}
|
|
@@ -15536,7 +16669,8 @@ ${xml}`
|
|
|
15536
16669
|
return {
|
|
15537
16670
|
nativeStreams,
|
|
15538
16671
|
rtmpStreams,
|
|
15539
|
-
rtspStreams
|
|
16672
|
+
rtspStreams,
|
|
16673
|
+
rawEncXml: widerMetadata?.rawXml
|
|
15540
16674
|
};
|
|
15541
16675
|
}
|
|
15542
16676
|
const guessRtspEncodingPrefix = (m) => {
|
|
@@ -15798,7 +16932,8 @@ ${xml}`
|
|
|
15798
16932
|
const result = {
|
|
15799
16933
|
nativeStreams,
|
|
15800
16934
|
rtmpStreams,
|
|
15801
|
-
rtspStreams
|
|
16935
|
+
rtspStreams,
|
|
16936
|
+
rawEncXml: streamMetadata?.rawXml
|
|
15802
16937
|
};
|
|
15803
16938
|
return cacheOrFallback(result);
|
|
15804
16939
|
}
|
|
@@ -15811,7 +16946,7 @@ ${xml}`
|
|
|
15811
16946
|
* @returns Test results for all stream types and profiles
|
|
15812
16947
|
*/
|
|
15813
16948
|
async testChannelStreams(channel, logger) {
|
|
15814
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
16949
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-6WEMO4L4.js");
|
|
15815
16950
|
return await testChannelStreams({
|
|
15816
16951
|
api: this,
|
|
15817
16952
|
channel: this.normalizeChannel(channel),
|
|
@@ -15827,7 +16962,7 @@ ${xml}`
|
|
|
15827
16962
|
* @returns Complete diagnostics for all channels and streams
|
|
15828
16963
|
*/
|
|
15829
16964
|
async collectMultifocalDiagnostics(logger) {
|
|
15830
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
16965
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-6WEMO4L4.js");
|
|
15831
16966
|
return await collectMultifocalDiagnostics({
|
|
15832
16967
|
api: this,
|
|
15833
16968
|
logger
|
|
@@ -16506,6 +17641,134 @@ ${xml}`
|
|
|
16506
17641
|
});
|
|
16507
17642
|
return parseXmlFragmentToJson(xml);
|
|
16508
17643
|
}
|
|
17644
|
+
/**
|
|
17645
|
+
* Build the UI-friendly user sessions strings directly in the library.
|
|
17646
|
+
* This is intended for hosts (like Scrypted plugins) that want a stable, consistent
|
|
17647
|
+
* sessions view without re-implementing parsing/formatting logic.
|
|
17648
|
+
*/
|
|
17649
|
+
async getOnlineUserSessionsForUi(options) {
|
|
17650
|
+
const sessions = await this.getOnlineUserList(options);
|
|
17651
|
+
let socketSessionId;
|
|
17652
|
+
try {
|
|
17653
|
+
socketSessionId = this.client?.getSocketSessionId?.();
|
|
17654
|
+
} catch {
|
|
17655
|
+
}
|
|
17656
|
+
const out = [];
|
|
17657
|
+
out.push(`Last updated: ${(/* @__PURE__ */ new Date()).toLocaleString()}`);
|
|
17658
|
+
if (socketSessionId) {
|
|
17659
|
+
out.push(`Current socket session ID: ${socketSessionId}`);
|
|
17660
|
+
}
|
|
17661
|
+
try {
|
|
17662
|
+
const summary = this.getDedicatedSessionsSummary();
|
|
17663
|
+
out.push(`Internal dedicated sessions (active): ${summary.count}`);
|
|
17664
|
+
for (const key of summary.keys) {
|
|
17665
|
+
out.push(` - ${key}`);
|
|
17666
|
+
}
|
|
17667
|
+
} catch {
|
|
17668
|
+
}
|
|
17669
|
+
out.push("");
|
|
17670
|
+
const looksLikeUserSession = (value) => {
|
|
17671
|
+
if (!value || typeof value !== "object") return false;
|
|
17672
|
+
const hasUser = value.userName !== void 0 || value.user !== void 0 || value.username !== void 0;
|
|
17673
|
+
const hasIp = value.ip !== void 0 || value.ipAddress !== void 0;
|
|
17674
|
+
const hasPort = value.port !== void 0;
|
|
17675
|
+
const hasSessionId = value.sessionId !== void 0;
|
|
17676
|
+
const hasId = value.id !== void 0;
|
|
17677
|
+
return hasUser && (hasIp || hasPort) || hasSessionId && (hasUser || hasIp || hasPort) || hasId && (hasUser || hasIp || hasPort);
|
|
17678
|
+
};
|
|
17679
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17680
|
+
const keyFor = (session, group) => {
|
|
17681
|
+
const user = session?.userName ?? session?.user ?? session?.username;
|
|
17682
|
+
const ip = session?.ip ?? session?.ipAddress;
|
|
17683
|
+
const port = session?.port;
|
|
17684
|
+
const sessionId = session?.sessionId;
|
|
17685
|
+
const id = session?.id;
|
|
17686
|
+
return `${group ?? ""}|u:${String(user ?? "")}@${String(ip ?? "")}:${String(
|
|
17687
|
+
port ?? ""
|
|
17688
|
+
)}|sid:${String(sessionId ?? "")}|id:${String(id ?? "")}`;
|
|
17689
|
+
};
|
|
17690
|
+
const collected = [];
|
|
17691
|
+
const collect = (data, group) => {
|
|
17692
|
+
if (!data) return;
|
|
17693
|
+
if (Array.isArray(data)) {
|
|
17694
|
+
for (const item of data) {
|
|
17695
|
+
if (looksLikeUserSession(item)) {
|
|
17696
|
+
const k = keyFor(item, group);
|
|
17697
|
+
if (!seen.has(k)) {
|
|
17698
|
+
seen.add(k);
|
|
17699
|
+
collected.push(
|
|
17700
|
+
group ? { session: item, group } : { session: item }
|
|
17701
|
+
);
|
|
17702
|
+
}
|
|
17703
|
+
} else if (item && typeof item === "object") {
|
|
17704
|
+
collect(item, group);
|
|
17705
|
+
}
|
|
17706
|
+
}
|
|
17707
|
+
return;
|
|
17708
|
+
}
|
|
17709
|
+
if (typeof data !== "object") return;
|
|
17710
|
+
let foundNested = false;
|
|
17711
|
+
for (const [k, v] of Object.entries(data)) {
|
|
17712
|
+
if (Array.isArray(v)) {
|
|
17713
|
+
foundNested = true;
|
|
17714
|
+
collect(v, k);
|
|
17715
|
+
} else if (v && typeof v === "object") {
|
|
17716
|
+
foundNested = true;
|
|
17717
|
+
collect(v, group);
|
|
17718
|
+
}
|
|
17719
|
+
}
|
|
17720
|
+
if (!foundNested && looksLikeUserSession(data)) {
|
|
17721
|
+
const k = keyFor(data, group);
|
|
17722
|
+
if (!seen.has(k)) {
|
|
17723
|
+
seen.add(k);
|
|
17724
|
+
collected.push(group ? { session: data, group } : { session: data });
|
|
17725
|
+
}
|
|
17726
|
+
}
|
|
17727
|
+
};
|
|
17728
|
+
const format = (session, index, group) => {
|
|
17729
|
+
const parts = [];
|
|
17730
|
+
if (group) parts.push(`[${group}]`);
|
|
17731
|
+
parts.push(`Session ${index}:`);
|
|
17732
|
+
if (session.userName !== void 0)
|
|
17733
|
+
parts.push(`User: ${session.userName}`);
|
|
17734
|
+
if (session.user !== void 0) parts.push(`User: ${session.user}`);
|
|
17735
|
+
if (session.ip !== void 0) parts.push(`IP: ${session.ip}`);
|
|
17736
|
+
if (session.ipAddress !== void 0)
|
|
17737
|
+
parts.push(`IP: ${session.ipAddress}`);
|
|
17738
|
+
if (session.port !== void 0) parts.push(`Port: ${session.port}`);
|
|
17739
|
+
if (session.sessionId !== void 0)
|
|
17740
|
+
parts.push(`Session ID: ${session.sessionId}`);
|
|
17741
|
+
if (session.id !== void 0) parts.push(`ID: ${session.id}`);
|
|
17742
|
+
if (session.loginTime !== void 0)
|
|
17743
|
+
parts.push(`Login Time: ${session.loginTime}`);
|
|
17744
|
+
if (session.time !== void 0) parts.push(`Time: ${session.time}`);
|
|
17745
|
+
if (session.status !== void 0) parts.push(`Status: ${session.status}`);
|
|
17746
|
+
if (parts.length === (group ? 2 : 1)) {
|
|
17747
|
+
if (session && typeof session === "object") {
|
|
17748
|
+
const allFields = Object.entries(session).filter(([, v]) => v === null || typeof v !== "object").map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
17749
|
+
if (allFields) parts.push(allFields);
|
|
17750
|
+
}
|
|
17751
|
+
}
|
|
17752
|
+
return parts.join(" | ");
|
|
17753
|
+
};
|
|
17754
|
+
collect(sessions);
|
|
17755
|
+
const onlineUserList = sessions.body?.OnlineUserList;
|
|
17756
|
+
const rawCurrent = onlineUserList?.OnlineUser;
|
|
17757
|
+
const rawLegacy = onlineUserList?.item;
|
|
17758
|
+
const currentItems = Array.isArray(rawCurrent) ? rawCurrent : rawCurrent ? [rawCurrent] : [];
|
|
17759
|
+
const legacyItems = Array.isArray(rawLegacy) ? rawLegacy : rawLegacy ? [rawLegacy] : [];
|
|
17760
|
+
const sessionIds = currentItems.map((s) => `${s.sessionId}@${s.ipAddress}`).join(", ");
|
|
17761
|
+
const legacyIds = legacyItems.map((s) => `${s.sessionId}@${s.ip}`).join(", ");
|
|
17762
|
+
this.logger.log?.(
|
|
17763
|
+
`[ReolinkBaichuanApi] getOnlineUserSessionsForUi: legacyItems=${legacyItems.length}, currentItems=${currentItems.length} [${sessionIds || legacyIds || "none"}]`
|
|
17764
|
+
);
|
|
17765
|
+
let count = 0;
|
|
17766
|
+
for (const entry of collected) {
|
|
17767
|
+
out.push(format(entry.session, ++count, entry.group));
|
|
17768
|
+
}
|
|
17769
|
+
if (count === 0) out.push("No active sessions found");
|
|
17770
|
+
return out;
|
|
17771
|
+
}
|
|
16509
17772
|
// Placeholder cmdIds seen in PCAPs but without XML samples yet.
|
|
16510
17773
|
// Expose as JSON (parsed from XML) for easy inspection.
|
|
16511
17774
|
async getCmd123(channel, options) {
|
|
@@ -16862,18 +18125,12 @@ ${scheduleItems}
|
|
|
16862
18125
|
...params.isNvr != null ? { isNvr: params.isNvr } : {},
|
|
16863
18126
|
...params.deviceId != null ? { deviceId: params.deviceId } : {}
|
|
16864
18127
|
};
|
|
16865
|
-
const {
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
logger?.debug?.(
|
|
16872
|
-
`[createRecordingReplayMp4Stream] startRecordingReplayStream failed; force-closing dedicated client and retrying once`
|
|
16873
|
-
);
|
|
16874
|
-
await this.forceCloseDedicatedClient(sessionKey, logger);
|
|
16875
|
-
return await this.startRecordingReplayStream(startParams);
|
|
16876
|
-
}
|
|
18128
|
+
const {
|
|
18129
|
+
result: replayResult,
|
|
18130
|
+
release: releaseQueueSlot,
|
|
18131
|
+
abortSignal
|
|
18132
|
+
} = await this.enqueueStreamingReplayOperation(async () => {
|
|
18133
|
+
return await this.startRecordingReplayStream(startParams);
|
|
16877
18134
|
});
|
|
16878
18135
|
const { stream, stop: stopReplay } = replayResult;
|
|
16879
18136
|
const input = new PassThrough();
|
|
@@ -17017,6 +18274,20 @@ ${scheduleItems}
|
|
|
17017
18274
|
},
|
|
17018
18275
|
Math.max(1, seconds) * 1e3
|
|
17019
18276
|
);
|
|
18277
|
+
if (abortSignal) {
|
|
18278
|
+
abortSignal.addEventListener(
|
|
18279
|
+
"abort",
|
|
18280
|
+
() => {
|
|
18281
|
+
if (!ended) {
|
|
18282
|
+
logger?.debug?.(
|
|
18283
|
+
`[createRecordingReplayMp4Stream] Abort signal received, stopping for new clip`
|
|
18284
|
+
);
|
|
18285
|
+
void stopAll();
|
|
18286
|
+
}
|
|
18287
|
+
},
|
|
18288
|
+
{ once: true }
|
|
18289
|
+
);
|
|
18290
|
+
}
|
|
17020
18291
|
output.on("close", () => {
|
|
17021
18292
|
clearTimeout(timer);
|
|
17022
18293
|
void stopAll();
|
|
@@ -17269,18 +18540,12 @@ ${scheduleItems}
|
|
|
17269
18540
|
...params.isNvr != null ? { isNvr: params.isNvr } : {},
|
|
17270
18541
|
...params.deviceId != null ? { deviceId: params.deviceId } : {}
|
|
17271
18542
|
};
|
|
17272
|
-
const {
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
logger?.debug?.(
|
|
17279
|
-
`[createRecordingReplayHlsSession] startRecordingReplayStream failed; force-closing dedicated client and retrying once`
|
|
17280
|
-
);
|
|
17281
|
-
await this.forceCloseDedicatedClient(sessionKey, logger);
|
|
17282
|
-
return await this.startRecordingReplayStream(startParams);
|
|
17283
|
-
}
|
|
18543
|
+
const {
|
|
18544
|
+
result: replayResult,
|
|
18545
|
+
release: releaseQueueSlot,
|
|
18546
|
+
abortSignal
|
|
18547
|
+
} = await this.enqueueStreamingReplayOperation(async () => {
|
|
18548
|
+
return await this.startRecordingReplayStream(startParams);
|
|
17284
18549
|
});
|
|
17285
18550
|
const { stream, stop: stopReplay } = replayResult;
|
|
17286
18551
|
const input = new PassThrough();
|
|
@@ -17465,6 +18730,20 @@ ${scheduleItems}
|
|
|
17465
18730
|
},
|
|
17466
18731
|
Math.max(1, seconds) * 1e3
|
|
17467
18732
|
);
|
|
18733
|
+
if (abortSignal) {
|
|
18734
|
+
abortSignal.addEventListener(
|
|
18735
|
+
"abort",
|
|
18736
|
+
() => {
|
|
18737
|
+
if (!ended) {
|
|
18738
|
+
logger?.debug?.(
|
|
18739
|
+
`[createRecordingReplayHlsSession] Abort signal received, stopping for new clip`
|
|
18740
|
+
);
|
|
18741
|
+
void stopAll();
|
|
18742
|
+
}
|
|
18743
|
+
},
|
|
18744
|
+
{ once: true }
|
|
18745
|
+
);
|
|
18746
|
+
}
|
|
17468
18747
|
stream.on("error", (e) => {
|
|
17469
18748
|
logger?.error?.(
|
|
17470
18749
|
`[createRecordingReplayHlsSession] Stream error: ${e.message}`
|
|
@@ -18206,7 +19485,9 @@ async function discoverUidForHost(host, logger) {
|
|
|
18206
19485
|
const directMatch = directDevices.find((d) => d.host === directTarget);
|
|
18207
19486
|
const directUid = normalizeUid(directMatch?.uid);
|
|
18208
19487
|
if (directUid) {
|
|
18209
|
-
logger?.log?.(
|
|
19488
|
+
logger?.log?.(
|
|
19489
|
+
`[AutoDetect] UID discovered via UDP direct: ${maskUid(directUid)}`
|
|
19490
|
+
);
|
|
18210
19491
|
return directUid;
|
|
18211
19492
|
}
|
|
18212
19493
|
} catch {
|
|
@@ -18220,7 +19501,9 @@ async function discoverUidForHost(host, logger) {
|
|
|
18220
19501
|
const match = devices.find((d) => d.host === ip || d.host === host);
|
|
18221
19502
|
const uid = normalizeUid(match?.uid);
|
|
18222
19503
|
if (uid) {
|
|
18223
|
-
logger?.log?.(
|
|
19504
|
+
logger?.log?.(
|
|
19505
|
+
`[AutoDetect] UID discovered via UDP broadcast: ${maskUid(uid)}`
|
|
19506
|
+
);
|
|
18224
19507
|
return uid;
|
|
18225
19508
|
}
|
|
18226
19509
|
} catch {
|
|
@@ -18236,7 +19519,13 @@ async function pingHost(host, timeoutMs = 3e3) {
|
|
|
18236
19519
|
return new Promise((resolve) => {
|
|
18237
19520
|
const { exec } = __require("child_process");
|
|
18238
19521
|
const platform = process.platform;
|
|
18239
|
-
const pingCmd = platform === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform === "darwin" ?
|
|
19522
|
+
const pingCmd = platform === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform === "darwin" ? (
|
|
19523
|
+
// macOS: -W is in milliseconds (Linux: seconds)
|
|
19524
|
+
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
19525
|
+
) : (
|
|
19526
|
+
// Linux/BSD-ish: -W is in seconds on most distros
|
|
19527
|
+
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
19528
|
+
);
|
|
18240
19529
|
exec(pingCmd, (error) => {
|
|
18241
19530
|
resolve(!error);
|
|
18242
19531
|
});
|
|
@@ -18259,15 +19548,20 @@ function createBaichuanApi(inputs, transport) {
|
|
|
18259
19548
|
return api2;
|
|
18260
19549
|
}
|
|
18261
19550
|
const uid = normalizeUid(inputs.uid);
|
|
18262
|
-
|
|
18263
|
-
|
|
19551
|
+
const isLocalDirect = inputs.udpDiscoveryMethod === "local-direct";
|
|
19552
|
+
if (!uid && !isLocalDirect) {
|
|
19553
|
+
throw new Error(
|
|
19554
|
+
"UID is required for UDP cameras (BCUDP) unless using local-direct discovery"
|
|
19555
|
+
);
|
|
18264
19556
|
}
|
|
18265
19557
|
const api = new ReolinkBaichuanApi({
|
|
18266
19558
|
...base,
|
|
18267
19559
|
transport: "udp",
|
|
18268
|
-
|
|
18269
|
-
...
|
|
18270
|
-
|
|
19560
|
+
// UID is optional for local-direct, required for other methods
|
|
19561
|
+
...uid ? { uid } : {},
|
|
19562
|
+
...inputs.udpDiscoveryMethod ? { udpDiscoveryMethod: inputs.udpDiscoveryMethod } : {}
|
|
19563
|
+
// NOTE: idleDisconnect is NOT enabled here - it will be enabled dynamically
|
|
19564
|
+
// after detecting if the device has a battery (via setIdleDisconnect)
|
|
18271
19565
|
});
|
|
18272
19566
|
attachErrorHandler(api, transport, inputs);
|
|
18273
19567
|
return api;
|
|
@@ -18280,7 +19574,9 @@ function attachErrorHandler(api, transport, inputs) {
|
|
|
18280
19574
|
if (typeof msg === "string" && (msg.includes("Baichuan socket closed") || msg.includes("Baichuan UDP stream closed") || msg.includes("Not running"))) {
|
|
18281
19575
|
return;
|
|
18282
19576
|
}
|
|
18283
|
-
inputs.logger?.log?.(
|
|
19577
|
+
inputs.logger?.log?.(
|
|
19578
|
+
`[BaichuanClient] error (${transport}) ${inputs.host}: ${msg}`
|
|
19579
|
+
);
|
|
18284
19580
|
});
|
|
18285
19581
|
api.client.on("close", () => {
|
|
18286
19582
|
});
|
|
@@ -18291,7 +19587,13 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18291
19587
|
const { host, uid, logger } = inputs;
|
|
18292
19588
|
const mode = inputs.mode ?? "auto";
|
|
18293
19589
|
const maxRetriesRaw = inputs.maxRetries;
|
|
18294
|
-
const maxRetries = Math.max(
|
|
19590
|
+
const maxRetries = Math.max(
|
|
19591
|
+
1,
|
|
19592
|
+
Math.min(
|
|
19593
|
+
10,
|
|
19594
|
+
typeof maxRetriesRaw === "number" && Number.isFinite(maxRetriesRaw) ? Math.floor(maxRetriesRaw) : 1
|
|
19595
|
+
)
|
|
19596
|
+
);
|
|
18295
19597
|
const fmtErr = (e) => {
|
|
18296
19598
|
const m = e?.message || e?.toString?.() || String(e);
|
|
18297
19599
|
return typeof m === "string" ? m : String(m);
|
|
@@ -18317,7 +19619,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18317
19619
|
lastErr = e;
|
|
18318
19620
|
const msg = fmtErr(e);
|
|
18319
19621
|
const retryable = attempt < max && shouldRetry(e);
|
|
18320
|
-
logger?.log?.(
|
|
19622
|
+
logger?.log?.(
|
|
19623
|
+
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
19624
|
+
);
|
|
18321
19625
|
if (!retryable) throw e;
|
|
18322
19626
|
await sleepMs2(350);
|
|
18323
19627
|
}
|
|
@@ -18328,15 +19632,21 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18328
19632
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
18329
19633
|
const isReachable = await pingHost(host);
|
|
18330
19634
|
if (!isReachable) {
|
|
18331
|
-
logger?.log?.(
|
|
19635
|
+
logger?.log?.(
|
|
19636
|
+
`[AutoDetect] Host ${host} is not reachable via ping, but continuing with connection attempt...`
|
|
19637
|
+
);
|
|
18332
19638
|
} else {
|
|
18333
19639
|
logger?.log?.(`[AutoDetect] Host ${host} is reachable`);
|
|
18334
19640
|
}
|
|
18335
19641
|
if (mode === "udp") {
|
|
18336
|
-
logger?.log?.(
|
|
19642
|
+
logger?.log?.(
|
|
19643
|
+
`[AutoDetect] Forced mode=udp, skipping TCP and starting UDP discovery/login...`
|
|
19644
|
+
);
|
|
18337
19645
|
let normalizedUid = effectiveUid;
|
|
18338
19646
|
if (!normalizedUid) {
|
|
18339
|
-
logger?.log?.(
|
|
19647
|
+
logger?.log?.(
|
|
19648
|
+
`[AutoDetect] UID not provided; attempting UDP discovery for UID...`
|
|
19649
|
+
);
|
|
18340
19650
|
const discovered = await discoverUidForHost(host, logger);
|
|
18341
19651
|
const normalizedDiscovered = normalizeUid(discovered);
|
|
18342
19652
|
if (!normalizedDiscovered) {
|
|
@@ -18355,13 +19665,18 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18355
19665
|
`UDP(${m})`,
|
|
18356
19666
|
maxRetries,
|
|
18357
19667
|
async (attempt) => {
|
|
18358
|
-
const api = createBaichuanApi(
|
|
19668
|
+
const api = createBaichuanApi(
|
|
19669
|
+
{ ...inputs, uid: normalizedUid, udpDiscoveryMethod: m },
|
|
19670
|
+
"udp"
|
|
19671
|
+
);
|
|
18359
19672
|
try {
|
|
18360
19673
|
await api.login();
|
|
18361
19674
|
return api;
|
|
18362
19675
|
} catch (e) {
|
|
18363
19676
|
try {
|
|
18364
|
-
await api.close({
|
|
19677
|
+
await api.close({
|
|
19678
|
+
reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
|
|
19679
|
+
});
|
|
18365
19680
|
} catch {
|
|
18366
19681
|
}
|
|
18367
19682
|
throw e;
|
|
@@ -18379,10 +19694,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18379
19694
|
const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
|
|
18380
19695
|
const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
|
|
18381
19696
|
const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
|
|
19697
|
+
const hasBattery = capabilities?.capabilities?.hasBattery === true;
|
|
18382
19698
|
if (isMultifocal) {
|
|
18383
19699
|
const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
|
|
18384
19700
|
logger?.log?.(
|
|
18385
|
-
`[AutoDetect] UDP (${m}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}).`
|
|
19701
|
+
`[AutoDetect] UDP (${m}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
|
|
18386
19702
|
);
|
|
18387
19703
|
return {
|
|
18388
19704
|
type: "multifocal",
|
|
@@ -18395,9 +19711,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18395
19711
|
api: udpApi
|
|
18396
19712
|
};
|
|
18397
19713
|
}
|
|
18398
|
-
|
|
19714
|
+
const deviceType = hasBattery ? "battery-cam" : "camera";
|
|
19715
|
+
logger?.log?.(
|
|
19716
|
+
`[AutoDetect] UDP (${m}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
|
|
19717
|
+
);
|
|
18399
19718
|
return {
|
|
18400
|
-
type:
|
|
19719
|
+
type: deviceType,
|
|
18401
19720
|
transport: "udp",
|
|
18402
19721
|
uid: normalizedUid,
|
|
18403
19722
|
udpDiscoveryMethod: m,
|
|
@@ -18412,7 +19731,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18412
19731
|
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
18413
19732
|
}
|
|
18414
19733
|
}
|
|
18415
|
-
throw new Error(
|
|
19734
|
+
throw new Error(
|
|
19735
|
+
`Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
|
|
19736
|
+
);
|
|
18416
19737
|
}
|
|
18417
19738
|
let tcpApi;
|
|
18418
19739
|
try {
|
|
@@ -18427,7 +19748,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18427
19748
|
return api2;
|
|
18428
19749
|
} catch (e) {
|
|
18429
19750
|
try {
|
|
18430
|
-
await api2.close({
|
|
19751
|
+
await api2.close({
|
|
19752
|
+
reason: `autodetect:tcp_failed:attempt_${attempt}`
|
|
19753
|
+
});
|
|
18431
19754
|
} catch {
|
|
18432
19755
|
}
|
|
18433
19756
|
throw e;
|
|
@@ -18436,7 +19759,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18436
19759
|
shouldRetryTcp
|
|
18437
19760
|
);
|
|
18438
19761
|
const api = tcpApi;
|
|
18439
|
-
if (!api)
|
|
19762
|
+
if (!api)
|
|
19763
|
+
throw new Error(
|
|
19764
|
+
"AutoDetect internal error: TCP API not initialized after successful login"
|
|
19765
|
+
);
|
|
18440
19766
|
const runProbeVariants = async (label, variants) => {
|
|
18441
19767
|
let lastMsg;
|
|
18442
19768
|
for (const v of variants) {
|
|
@@ -18447,30 +19773,67 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18447
19773
|
} catch (e) {
|
|
18448
19774
|
const msg = fmtErr(e);
|
|
18449
19775
|
lastMsg = msg;
|
|
18450
|
-
logger?.log?.(
|
|
19776
|
+
logger?.log?.(
|
|
19777
|
+
`[AutoDetect] TCP probe ${label} failed (${v.variant}): ${msg}`
|
|
19778
|
+
);
|
|
18451
19779
|
}
|
|
18452
19780
|
}
|
|
18453
19781
|
if (lastMsg) {
|
|
18454
|
-
logger?.log?.(
|
|
19782
|
+
logger?.log?.(
|
|
19783
|
+
`[AutoDetect] TCP probe ${label} failed (all variants): ${lastMsg}`
|
|
19784
|
+
);
|
|
18455
19785
|
}
|
|
18456
19786
|
return void 0;
|
|
18457
19787
|
};
|
|
18458
19788
|
const infoProbe = await runProbeVariants(
|
|
18459
19789
|
"getInfo",
|
|
18460
19790
|
[
|
|
18461
|
-
{
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
19791
|
+
{
|
|
19792
|
+
variant: "cmd80 class=0x6414",
|
|
19793
|
+
op: () => api.getInfo(void 0, {
|
|
19794
|
+
timeoutMs: 2500,
|
|
19795
|
+
messageClass: BC_CLASS_MODERN_24
|
|
19796
|
+
})
|
|
19797
|
+
},
|
|
19798
|
+
{
|
|
19799
|
+
variant: "cmd80 class=0x6614",
|
|
19800
|
+
op: () => api.getInfo(void 0, {
|
|
19801
|
+
timeoutMs: 3e3,
|
|
19802
|
+
messageClass: BC_CLASS_MODERN_20
|
|
19803
|
+
})
|
|
19804
|
+
},
|
|
19805
|
+
{
|
|
19806
|
+
variant: "cmd318(ch0) class=0x6414",
|
|
19807
|
+
op: () => api.getInfo(0, {
|
|
19808
|
+
timeoutMs: 3e3,
|
|
19809
|
+
messageClass: BC_CLASS_MODERN_24
|
|
19810
|
+
})
|
|
19811
|
+
},
|
|
19812
|
+
{
|
|
19813
|
+
variant: "cmd318(ch0) class=0x6614",
|
|
19814
|
+
op: () => api.getInfo(0, {
|
|
19815
|
+
timeoutMs: 3500,
|
|
19816
|
+
messageClass: BC_CLASS_MODERN_20
|
|
19817
|
+
})
|
|
19818
|
+
}
|
|
18472
19819
|
]
|
|
18473
19820
|
);
|
|
19821
|
+
const supportProbe = await runProbeVariants("getSupportInfo", [
|
|
19822
|
+
{
|
|
19823
|
+
variant: "cmd199 class=0x6414",
|
|
19824
|
+
op: () => api.getSupportInfo({
|
|
19825
|
+
timeoutMs: 2500,
|
|
19826
|
+
messageClass: BC_CLASS_MODERN_24
|
|
19827
|
+
})
|
|
19828
|
+
},
|
|
19829
|
+
{
|
|
19830
|
+
variant: "cmd199 class=0x6614",
|
|
19831
|
+
op: () => api.getSupportInfo({
|
|
19832
|
+
timeoutMs: 3500,
|
|
19833
|
+
messageClass: BC_CLASS_MODERN_20
|
|
19834
|
+
})
|
|
19835
|
+
}
|
|
19836
|
+
]);
|
|
18474
19837
|
const deviceInfo = infoProbe?.value;
|
|
18475
19838
|
const support = supportProbe?.value;
|
|
18476
19839
|
const channelNumRaw = support?.channelNum;
|
|
@@ -18487,7 +19850,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18487
19850
|
const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
|
|
18488
19851
|
if (isMultifocal) {
|
|
18489
19852
|
const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
|
|
18490
|
-
logger?.log?.(
|
|
19853
|
+
logger?.log?.(
|
|
19854
|
+
`[AutoDetect] Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum})`
|
|
19855
|
+
);
|
|
18491
19856
|
return {
|
|
18492
19857
|
type: "multifocal",
|
|
18493
19858
|
transport: "tcp",
|
|
@@ -18499,7 +19864,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18499
19864
|
};
|
|
18500
19865
|
}
|
|
18501
19866
|
if (effectiveChannelNum > 1) {
|
|
18502
|
-
logger?.log?.(
|
|
19867
|
+
logger?.log?.(
|
|
19868
|
+
`[AutoDetect] Detected NVR (${effectiveChannelNum} channels)`
|
|
19869
|
+
);
|
|
18503
19870
|
return {
|
|
18504
19871
|
type: "nvr",
|
|
18505
19872
|
transport: "tcp",
|
|
@@ -18533,23 +19900,27 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18533
19900
|
if (!isTcpFailureThatShouldFallbackToUdp(tcpError)) {
|
|
18534
19901
|
throw tcpError;
|
|
18535
19902
|
}
|
|
18536
|
-
logger?.log?.(`[AutoDetect] TCP failed, trying UDP
|
|
19903
|
+
logger?.log?.(`[AutoDetect] TCP failed, trying UDP...`);
|
|
18537
19904
|
let normalizedUid = effectiveUid;
|
|
18538
19905
|
if (!normalizedUid) {
|
|
18539
|
-
logger?.log?.(
|
|
19906
|
+
logger?.log?.(
|
|
19907
|
+
`[AutoDetect] UID not provided; attempting UDP broadcast discovery for UID...`
|
|
19908
|
+
);
|
|
18540
19909
|
const discovered = await discoverUidForHost(host, logger);
|
|
18541
|
-
if (
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
19910
|
+
if (discovered) {
|
|
19911
|
+
const normalizedDiscovered = normalizeUid(discovered);
|
|
19912
|
+
if (normalizedDiscovered) {
|
|
19913
|
+
normalizedUid = normalizedDiscovered;
|
|
19914
|
+
logger?.log?.(
|
|
19915
|
+
`[AutoDetect] UID discovered via broadcast: ${normalizedUid}`
|
|
19916
|
+
);
|
|
19917
|
+
}
|
|
18545
19918
|
}
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
`TCP connection failed and device likely requires UDP/BCUDP, but UID discovery returned an empty UID (ip=${host}).`
|
|
19919
|
+
if (!normalizedUid) {
|
|
19920
|
+
logger?.log?.(
|
|
19921
|
+
`[AutoDetect] UID discovery failed; will try local-direct without UID first.`
|
|
18550
19922
|
);
|
|
18551
19923
|
}
|
|
18552
|
-
normalizedUid = normalizedDiscovered;
|
|
18553
19924
|
}
|
|
18554
19925
|
try {
|
|
18555
19926
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
@@ -18563,31 +19934,38 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18563
19934
|
const channelNumValue = typeof channelNum === "string" ? Number.parseInt(channelNum, 10) : channelNum;
|
|
18564
19935
|
const hasDualLensChannelCount = (channelNumValue === 2 || channelNumValue === 3) && Number.isFinite(channelNumValue);
|
|
18565
19936
|
const isMultifocal = isMultifocalByModel || hasDualLensChannelCount;
|
|
19937
|
+
const hasBattery = capabilities?.capabilities?.hasBattery === true;
|
|
19938
|
+
udpApi.setIdleDisconnect(hasBattery);
|
|
18566
19939
|
if (isMultifocal) {
|
|
18567
19940
|
const detectionMethod = isMultifocalByModel ? "model match" : "channelNum fallback";
|
|
18568
19941
|
logger?.log?.(
|
|
18569
|
-
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}).`
|
|
19942
|
+
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected multi-focal device (${detectionMethod}: model=${normalizedModel ?? "unknown"}, channelNum=${channelNum}, hasBattery=${hasBattery}).`
|
|
18570
19943
|
);
|
|
18571
19944
|
return {
|
|
18572
19945
|
type: "multifocal",
|
|
18573
19946
|
transport: "udp",
|
|
18574
|
-
uid: normalizedUid,
|
|
19947
|
+
uid: normalizedUid ?? "",
|
|
18575
19948
|
udpDiscoveryMethod,
|
|
18576
19949
|
deviceInfo,
|
|
18577
19950
|
...hostNetworkInfo ? { hostNetworkInfo } : {},
|
|
18578
19951
|
channelNum,
|
|
19952
|
+
hasBattery,
|
|
18579
19953
|
api: udpApi
|
|
18580
19954
|
};
|
|
18581
19955
|
}
|
|
18582
|
-
|
|
19956
|
+
const deviceType = hasBattery ? "battery-cam" : "udp-camera";
|
|
19957
|
+
logger?.log?.(
|
|
19958
|
+
`[AutoDetect] UDP (${udpDiscoveryMethod}) connection successful. Detected ${deviceType} (hasBattery=${hasBattery}, model=${normalizedModel ?? "unknown"}).`
|
|
19959
|
+
);
|
|
18583
19960
|
return {
|
|
18584
|
-
type:
|
|
19961
|
+
type: deviceType,
|
|
18585
19962
|
transport: "udp",
|
|
18586
|
-
uid: normalizedUid,
|
|
19963
|
+
uid: normalizedUid ?? "",
|
|
18587
19964
|
udpDiscoveryMethod,
|
|
18588
19965
|
deviceInfo,
|
|
18589
19966
|
...hostNetworkInfo ? { hostNetworkInfo } : {},
|
|
18590
19967
|
channelNum: 1,
|
|
19968
|
+
hasBattery,
|
|
18591
19969
|
api: udpApi
|
|
18592
19970
|
};
|
|
18593
19971
|
};
|
|
@@ -18600,13 +19978,22 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18600
19978
|
`UDP(${m})`,
|
|
18601
19979
|
maxRetries,
|
|
18602
19980
|
async (attempt) => {
|
|
18603
|
-
const
|
|
19981
|
+
const apiInputs = {
|
|
19982
|
+
...inputs,
|
|
19983
|
+
udpDiscoveryMethod: m
|
|
19984
|
+
};
|
|
19985
|
+
if (normalizedUid) {
|
|
19986
|
+
apiInputs.uid = normalizedUid;
|
|
19987
|
+
}
|
|
19988
|
+
const api = createBaichuanApi(apiInputs, "udp");
|
|
18604
19989
|
try {
|
|
18605
19990
|
await api.login();
|
|
18606
19991
|
return api;
|
|
18607
19992
|
} catch (e) {
|
|
18608
19993
|
try {
|
|
18609
|
-
await api.close({
|
|
19994
|
+
await api.close({
|
|
19995
|
+
reason: `autodetect:udp_failed:${m}:attempt_${attempt}`
|
|
19996
|
+
});
|
|
18610
19997
|
} catch {
|
|
18611
19998
|
}
|
|
18612
19999
|
throw e;
|
|
@@ -18624,7 +20011,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
18624
20011
|
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
18625
20012
|
}
|
|
18626
20013
|
}
|
|
18627
|
-
throw new Error(
|
|
20014
|
+
throw new Error(
|
|
20015
|
+
`UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
|
|
20016
|
+
);
|
|
18628
20017
|
} catch (udpError) {
|
|
18629
20018
|
logger?.log?.(
|
|
18630
20019
|
`[AutoDetect] Both TCP and UDP failed. TCP error: ${tcpError}, UDP error: ${udpError}`
|
|
@@ -18676,4 +20065,4 @@ export {
|
|
|
18676
20065
|
isTcpFailureThatShouldFallbackToUdp,
|
|
18677
20066
|
autoDetectDeviceType
|
|
18678
20067
|
};
|
|
18679
|
-
//# sourceMappingURL=chunk-
|
|
20068
|
+
//# sourceMappingURL=chunk-ULSFEQSE.js.map
|