@apocaliss92/nodelink-js 0.1.20 → 0.2.2
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 +20 -1
- package/dist/{DiagnosticsTools-NUMCYEKQ.js → DiagnosticsTools-FNLGCOVA.js} +2 -2
- package/dist/{chunk-EHWVA3SG.js → chunk-MN7GUZT7.js} +981 -250
- package/dist/chunk-MN7GUZT7.js.map +1 -0
- package/dist/{chunk-YPU7RAEY.js → chunk-NLTB7GTA.js} +17 -1
- package/dist/{chunk-YPU7RAEY.js.map → chunk-NLTB7GTA.js.map} +1 -1
- package/dist/cli/rtsp-server.cjs +978 -247
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +1002 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -16
- package/dist/index.d.ts +260 -15
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-EHWVA3SG.js.map +0 -1
- /package/dist/{DiagnosticsTools-NUMCYEKQ.js.map → DiagnosticsTools-FNLGCOVA.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -60,7 +60,7 @@ var init_urls = __esm({
|
|
|
60
60
|
function bcHeaderHasPayloadOffset(messageClass) {
|
|
61
61
|
return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
|
|
62
62
|
}
|
|
63
|
-
var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST;
|
|
63
|
+
var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
|
|
64
64
|
var init_constants = __esm({
|
|
65
65
|
"src/protocol/constants.ts"() {
|
|
66
66
|
"use strict";
|
|
@@ -164,6 +164,14 @@ var init_constants = __esm({
|
|
|
164
164
|
BC_CMD_ID_PUSH_DINGDONG_LIST = 484;
|
|
165
165
|
BC_CMD_ID_PUSH_SLEEP_STATUS = 623;
|
|
166
166
|
BC_CMD_ID_PUSH_COORDINATE_POINT_LIST = 723;
|
|
167
|
+
BC_CMD_ID_DING_DONG_CTRL = 483;
|
|
168
|
+
BC_CMD_ID_GET_DING_DONG_LIST = 484;
|
|
169
|
+
BC_CMD_ID_DING_DONG_OPT = 485;
|
|
170
|
+
BC_CMD_ID_GET_DING_DONG_CFG = 486;
|
|
171
|
+
BC_CMD_ID_SET_DING_DONG_CFG = 487;
|
|
172
|
+
BC_CMD_ID_QUICK_REPLY_PLAY = 349;
|
|
173
|
+
BC_CMD_ID_GET_DING_DONG_SILENT = 609;
|
|
174
|
+
BC_CMD_ID_SET_DING_DONG_SILENT = 610;
|
|
167
175
|
}
|
|
168
176
|
});
|
|
169
177
|
|
|
@@ -7542,6 +7550,8 @@ var NativeStreamFanout = class {
|
|
|
7542
7550
|
} finally {
|
|
7543
7551
|
for (const q of this.queues.values()) q.close();
|
|
7544
7552
|
this.queues.clear();
|
|
7553
|
+
this.running = false;
|
|
7554
|
+
this.opts.onEnd?.();
|
|
7545
7555
|
}
|
|
7546
7556
|
})();
|
|
7547
7557
|
}
|
|
@@ -7867,7 +7877,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7867
7877
|
this.logger.warn(
|
|
7868
7878
|
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
7869
7879
|
);
|
|
7870
|
-
this.streamMetadata = { frameRate: 25
|
|
7880
|
+
this.streamMetadata = { frameRate: 25 };
|
|
7871
7881
|
this.setFlowVideoType("H264", "metadata unavailable");
|
|
7872
7882
|
}
|
|
7873
7883
|
this.clientConnectionServer = net.createServer((socket) => {
|
|
@@ -7899,7 +7909,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7899
7909
|
*/
|
|
7900
7910
|
handleRtspConnection(socket) {
|
|
7901
7911
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
7902
|
-
|
|
7912
|
+
const connectTime = Date.now();
|
|
7913
|
+
this.logger.info(
|
|
7914
|
+
`[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
|
|
7915
|
+
);
|
|
7903
7916
|
let sessionId = "";
|
|
7904
7917
|
let buffer = Buffer.alloc(0);
|
|
7905
7918
|
let clientFfmpeg;
|
|
@@ -7907,6 +7920,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7907
7920
|
let clientUdpSocket = null;
|
|
7908
7921
|
let clientUdpSocketAudio = null;
|
|
7909
7922
|
const cleanup = () => {
|
|
7923
|
+
const sessionDurationMs = Date.now() - connectTime;
|
|
7924
|
+
const res = this.clientResources.get(clientId);
|
|
7925
|
+
const framesSent = res?.framesSent ?? 0;
|
|
7926
|
+
this.logger.info(
|
|
7927
|
+
`[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
|
|
7928
|
+
);
|
|
7910
7929
|
this.removeClient(clientId);
|
|
7911
7930
|
this.authNonces.delete(clientId);
|
|
7912
7931
|
const resources = this.clientResources.get(clientId);
|
|
@@ -8048,7 +8067,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8048
8067
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
8049
8068
|
});
|
|
8050
8069
|
} else if (method === "DESCRIBE") {
|
|
8051
|
-
if (!this.
|
|
8070
|
+
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
8052
8071
|
try {
|
|
8053
8072
|
if (!this.nativeStreamActive) {
|
|
8054
8073
|
await this.startNativeStream();
|
|
@@ -8130,7 +8149,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8130
8149
|
seenFirstVideoKeyframe: false,
|
|
8131
8150
|
setupTrack0: false,
|
|
8132
8151
|
setupTrack1: false,
|
|
8133
|
-
isPlaying: false
|
|
8152
|
+
isPlaying: false,
|
|
8153
|
+
connectTime
|
|
8134
8154
|
});
|
|
8135
8155
|
} else {
|
|
8136
8156
|
existing.rtspSocket = socket;
|
|
@@ -8177,8 +8197,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8177
8197
|
if (resources) {
|
|
8178
8198
|
if (isTrack1) resources.setupTrack1 = true;
|
|
8179
8199
|
else resources.setupTrack0 = true;
|
|
8180
|
-
|
|
8181
|
-
|
|
8200
|
+
const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
|
|
8201
|
+
const track = isTrack1 ? "track1(audio)" : "track0(video)";
|
|
8202
|
+
this.logger.info(
|
|
8203
|
+
`[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
|
|
8182
8204
|
);
|
|
8183
8205
|
}
|
|
8184
8206
|
}
|
|
@@ -8203,8 +8225,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8203
8225
|
const resources = this.clientResources.get(clientId);
|
|
8204
8226
|
if (resources) {
|
|
8205
8227
|
resources.isPlaying = true;
|
|
8206
|
-
|
|
8207
|
-
|
|
8228
|
+
const hasAudio = !!resources.setupTrack1;
|
|
8229
|
+
this.logger.info(
|
|
8230
|
+
`[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
|
|
8208
8231
|
);
|
|
8209
8232
|
}
|
|
8210
8233
|
}
|
|
@@ -8213,6 +8236,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8213
8236
|
Range: "npt=0.000-"
|
|
8214
8237
|
});
|
|
8215
8238
|
} else if (method === "TEARDOWN") {
|
|
8239
|
+
this.logger.info(
|
|
8240
|
+
`[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
|
|
8241
|
+
);
|
|
8216
8242
|
cleanup();
|
|
8217
8243
|
sendResponse(200, "OK", {
|
|
8218
8244
|
Session: sessionId
|
|
@@ -8307,7 +8333,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8307
8333
|
this.logger.warn(
|
|
8308
8334
|
`[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
|
|
8309
8335
|
);
|
|
8310
|
-
streamMetadata = { frameRate: 25
|
|
8336
|
+
streamMetadata = { frameRate: 25 };
|
|
8311
8337
|
}
|
|
8312
8338
|
}
|
|
8313
8339
|
const ffmpegFormat = this.flow.ffmpegFormat;
|
|
@@ -8895,15 +8921,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8895
8921
|
`Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
|
|
8896
8922
|
);
|
|
8897
8923
|
}
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8924
|
+
if (!useDirectRtp) {
|
|
8925
|
+
const now = Date.now();
|
|
8926
|
+
const timeSinceLastFrame = now - lastFrameTime;
|
|
8927
|
+
const waitTime = targetFrameInterval - timeSinceLastFrame;
|
|
8928
|
+
if (waitTime > 0) {
|
|
8929
|
+
await new Promise(
|
|
8930
|
+
(resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
|
|
8931
|
+
);
|
|
8932
|
+
}
|
|
8933
|
+
lastFrameTime = Date.now();
|
|
8905
8934
|
}
|
|
8906
|
-
lastFrameTime = Date.now();
|
|
8907
8935
|
if (useDirectRtp) {
|
|
8908
8936
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
8909
8937
|
const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
|
|
@@ -8976,6 +9004,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8976
9004
|
}
|
|
8977
9005
|
if (!firstVideoWriteLogged) {
|
|
8978
9006
|
firstVideoWriteLogged = true;
|
|
9007
|
+
const clientConnectTime = resources?.connectTime ?? Date.now();
|
|
9008
|
+
const ttffMs = Date.now() - clientConnectTime;
|
|
9009
|
+
this.logger.info(
|
|
9010
|
+
`[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
|
|
9011
|
+
);
|
|
8979
9012
|
if (rtspDebug) {
|
|
8980
9013
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
8981
9014
|
rtspDebugLog(
|
|
@@ -8983,6 +9016,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8983
9016
|
);
|
|
8984
9017
|
}
|
|
8985
9018
|
}
|
|
9019
|
+
if (resources) {
|
|
9020
|
+
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
9021
|
+
}
|
|
8986
9022
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
8987
9023
|
} else {
|
|
8988
9024
|
try {
|
|
@@ -9067,8 +9103,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9067
9103
|
this.firstAudioPromise = new Promise((resolve) => {
|
|
9068
9104
|
this.firstAudioResolve = resolve;
|
|
9069
9105
|
});
|
|
9070
|
-
this.
|
|
9071
|
-
`
|
|
9106
|
+
this.logger.info(
|
|
9107
|
+
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
9072
9108
|
);
|
|
9073
9109
|
await this.flow.startKeepAlive(this.api);
|
|
9074
9110
|
this.nativeFanout = new NativeStreamFanout({
|
|
@@ -9111,6 +9147,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9111
9147
|
this.logger.warn(
|
|
9112
9148
|
`[BaichuanRtspServer] Shared native stream error: ${error}`
|
|
9113
9149
|
);
|
|
9150
|
+
},
|
|
9151
|
+
onEnd: () => {
|
|
9152
|
+
if (!this.nativeStreamActive) return;
|
|
9153
|
+
this.nativeStreamActive = false;
|
|
9154
|
+
this.firstFrameReceived = false;
|
|
9155
|
+
this.firstFramePromise = null;
|
|
9156
|
+
this.firstFrameResolve = null;
|
|
9157
|
+
this.nativeFanout = null;
|
|
9158
|
+
this.logger.info(
|
|
9159
|
+
`[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
9160
|
+
);
|
|
9161
|
+
if (this.connectedClients.size > 0) {
|
|
9162
|
+
this.logger.info(
|
|
9163
|
+
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
9164
|
+
);
|
|
9165
|
+
setImmediate(() => void this.startNativeStream());
|
|
9166
|
+
}
|
|
9114
9167
|
}
|
|
9115
9168
|
});
|
|
9116
9169
|
this.nativeFanout.start();
|
|
@@ -9149,7 +9202,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9149
9202
|
if (!this.nativeStreamActive) {
|
|
9150
9203
|
return;
|
|
9151
9204
|
}
|
|
9152
|
-
this.
|
|
9205
|
+
this.logger.info(
|
|
9206
|
+
`[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
9207
|
+
);
|
|
9153
9208
|
this.flow.stopKeepAlive();
|
|
9154
9209
|
this.clearNoClientAutoStopTimer();
|
|
9155
9210
|
this.nativeStreamActive = false;
|
|
@@ -9183,9 +9238,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9183
9238
|
if (this.connectedClients.has(clientId)) {
|
|
9184
9239
|
this.connectedClients.delete(clientId);
|
|
9185
9240
|
this.emit("clientDisconnected", clientId);
|
|
9186
|
-
this.logger.info(
|
|
9187
|
-
`[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
|
|
9188
|
-
);
|
|
9189
9241
|
if (this.connectedClients.size === 0) {
|
|
9190
9242
|
void this.stopNativeStream();
|
|
9191
9243
|
}
|
|
@@ -11146,6 +11198,13 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
11146
11198
|
* and may leave device-side sessions in a bad state.
|
|
11147
11199
|
*/
|
|
11148
11200
|
static coverPreviewQueueTail = /* @__PURE__ */ new Map();
|
|
11201
|
+
/**
|
|
11202
|
+
* Global CoverPreview backoff – increases on 400 rejection, resets on success.
|
|
11203
|
+
* Prevents flooding the camera when it's overwhelmed.
|
|
11204
|
+
*/
|
|
11205
|
+
static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
|
|
11206
|
+
static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
|
|
11207
|
+
static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
|
|
11149
11208
|
opts;
|
|
11150
11209
|
debugCfg;
|
|
11151
11210
|
logger;
|
|
@@ -11293,7 +11352,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
11293
11352
|
if (!this.isIdleDisconnectEnabled()) return false;
|
|
11294
11353
|
if (!this.isSocketConnected()) return false;
|
|
11295
11354
|
if (this.pending.size > 0) return false;
|
|
11296
|
-
if (this.
|
|
11355
|
+
if (this.isDeviceStreamingActive()) return false;
|
|
11297
11356
|
if (this.permits.size > 0) return false;
|
|
11298
11357
|
return true;
|
|
11299
11358
|
}
|
|
@@ -11307,7 +11366,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
11307
11366
|
const delayMs = Math.max(0, timeoutMs - elapsedMs);
|
|
11308
11367
|
this.idleDisconnectTimer = setTimeout(() => {
|
|
11309
11368
|
try {
|
|
11310
|
-
if (!this.isIdleDisconnectEligibleNow())
|
|
11369
|
+
if (!this.isIdleDisconnectEligibleNow()) {
|
|
11370
|
+
this.logDebug("idle_disconnect_blocked", {
|
|
11371
|
+
reason: "not eligible",
|
|
11372
|
+
socketConnected: this.isSocketConnected(),
|
|
11373
|
+
pending: this.pending.size,
|
|
11374
|
+
deviceStreamingActive: this.isDeviceStreamingActive(),
|
|
11375
|
+
localVideoSubs: this.hasActiveVideoSubscriptionsInternal(),
|
|
11376
|
+
permits: this.permits.size,
|
|
11377
|
+
host: this.opts.host
|
|
11378
|
+
});
|
|
11379
|
+
return;
|
|
11380
|
+
}
|
|
11311
11381
|
if (this.lastUserActivityAtMs == null) return;
|
|
11312
11382
|
const elapsed2 = Date.now() - this.lastUserActivityAtMs;
|
|
11313
11383
|
if (elapsed2 < timeoutMs) {
|
|
@@ -11421,7 +11491,34 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
11421
11491
|
async withSerializedCoverPreview(fn) {
|
|
11422
11492
|
const key = this.getCoverPreviewQueueKey();
|
|
11423
11493
|
const prevTail = _BaichuanClient.coverPreviewQueueTail.get(key) ?? Promise.resolve();
|
|
11424
|
-
const run = prevTail.catch(() => void 0).then(
|
|
11494
|
+
const run = prevTail.catch(() => void 0).then(async () => {
|
|
11495
|
+
const backoffMs = _BaichuanClient.coverPreviewBackoffMs.get(key) ?? 0;
|
|
11496
|
+
if (backoffMs > 0) {
|
|
11497
|
+
this.logDebug("coverpreview_backoff_wait", { backoffMs });
|
|
11498
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
11499
|
+
}
|
|
11500
|
+
try {
|
|
11501
|
+
const result = await fn();
|
|
11502
|
+
_BaichuanClient.coverPreviewBackoffMs.delete(key);
|
|
11503
|
+
return result;
|
|
11504
|
+
} catch (e) {
|
|
11505
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
11506
|
+
const is400 = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
|
|
11507
|
+
if (is400) {
|
|
11508
|
+
const current = _BaichuanClient.coverPreviewBackoffMs.get(key) ?? 0;
|
|
11509
|
+
const next = current === 0 ? _BaichuanClient.COVER_PREVIEW_INITIAL_BACKOFF_MS : Math.min(
|
|
11510
|
+
current * 2,
|
|
11511
|
+
_BaichuanClient.COVER_PREVIEW_MAX_BACKOFF_MS
|
|
11512
|
+
);
|
|
11513
|
+
_BaichuanClient.coverPreviewBackoffMs.set(key, next);
|
|
11514
|
+
this.logDebug("coverpreview_backoff_increased", {
|
|
11515
|
+
previous: current,
|
|
11516
|
+
next
|
|
11517
|
+
});
|
|
11518
|
+
}
|
|
11519
|
+
throw e;
|
|
11520
|
+
}
|
|
11521
|
+
});
|
|
11425
11522
|
const tail = run.then(
|
|
11426
11523
|
() => void 0,
|
|
11427
11524
|
() => void 0
|
|
@@ -13743,32 +13840,36 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
13743
13840
|
* Send CoverPreview command (cmd_id=298) to get an I-frame from a past recording.
|
|
13744
13841
|
* Similar to sendBinarySnapshot109 but handles the stream header + frame format
|
|
13745
13842
|
* instead of JPEG.
|
|
13843
|
+
*
|
|
13844
|
+
* Retry is minimal (2 attempts) – the global backoff in `withSerializedCoverPreview`
|
|
13845
|
+
* throttles subsequent requests when the camera is overwhelmed.
|
|
13846
|
+
* PCAP analysis shows the camera routinely rejects the first request with 400.
|
|
13746
13847
|
*/
|
|
13747
13848
|
async sendBinaryCoverPreview(params) {
|
|
13748
13849
|
return await this.withSerializedCoverPreview(async () => {
|
|
13749
|
-
const
|
|
13750
|
-
const
|
|
13850
|
+
const maxAttempts = 5;
|
|
13851
|
+
const retryDelay = 1500;
|
|
13751
13852
|
let lastError;
|
|
13752
|
-
for (let attempt = 0; attempt <
|
|
13853
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
13753
13854
|
try {
|
|
13754
13855
|
return await this._sendBinaryCoverPreviewOnce(params);
|
|
13755
13856
|
} catch (e) {
|
|
13756
13857
|
const msg = e instanceof Error ? e.message : String(e);
|
|
13757
13858
|
lastError = e instanceof Error ? e : new Error(msg);
|
|
13758
|
-
const
|
|
13759
|
-
if (
|
|
13859
|
+
const is400 = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
|
|
13860
|
+
if (is400 && attempt < maxAttempts - 1) {
|
|
13760
13861
|
this.logDebug("coverpreview_retry_400", {
|
|
13761
13862
|
attempt: attempt + 1,
|
|
13762
|
-
|
|
13763
|
-
|
|
13863
|
+
maxAttempts,
|
|
13864
|
+
retryDelay
|
|
13764
13865
|
});
|
|
13765
|
-
await new Promise((
|
|
13866
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
13766
13867
|
continue;
|
|
13767
13868
|
}
|
|
13768
13869
|
throw lastError;
|
|
13769
13870
|
}
|
|
13770
13871
|
}
|
|
13771
|
-
throw lastError ?? new Error("CoverPreview failed after all
|
|
13872
|
+
throw lastError ?? new Error("CoverPreview failed after all attempts");
|
|
13772
13873
|
});
|
|
13773
13874
|
}
|
|
13774
13875
|
/**
|
|
@@ -14343,10 +14444,12 @@ function parseSupportXml(xml) {
|
|
|
14343
14444
|
}
|
|
14344
14445
|
function getSupportItemForChannel(support, channel) {
|
|
14345
14446
|
if (!support?.items?.length) return void 0;
|
|
14346
|
-
const
|
|
14447
|
+
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
14448
|
+
if (!candidates.length) return void 0;
|
|
14449
|
+
const score = (item) => {
|
|
14347
14450
|
const anyItem = item;
|
|
14348
|
-
let
|
|
14349
|
-
if (anyItem.name == null)
|
|
14451
|
+
let result = 0;
|
|
14452
|
+
if (anyItem.name == null) result += 100;
|
|
14350
14453
|
const capabilityKeys = [
|
|
14351
14454
|
"ptzType",
|
|
14352
14455
|
"ptzControl",
|
|
@@ -14358,20 +14461,17 @@ function getSupportItemForChannel(support, channel) {
|
|
|
14358
14461
|
"motion",
|
|
14359
14462
|
"encCtrl",
|
|
14360
14463
|
"newIspCfg",
|
|
14361
|
-
"remoteAbility"
|
|
14464
|
+
"remoteAbility",
|
|
14465
|
+
"aitype",
|
|
14466
|
+
"videoClip",
|
|
14467
|
+
"snap"
|
|
14362
14468
|
];
|
|
14363
14469
|
for (const k of capabilityKeys) {
|
|
14364
|
-
if (anyItem[k] !== void 0)
|
|
14470
|
+
if (anyItem[k] !== void 0) result += 3;
|
|
14365
14471
|
}
|
|
14366
|
-
|
|
14367
|
-
return score;
|
|
14368
|
-
};
|
|
14369
|
-
const pickBest = (chnId) => {
|
|
14370
|
-
const candidates = support.items.filter((i) => i.chnID === chnId);
|
|
14371
|
-
if (!candidates.length) return void 0;
|
|
14372
|
-
return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
|
|
14472
|
+
return result;
|
|
14373
14473
|
};
|
|
14374
|
-
return
|
|
14474
|
+
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
14375
14475
|
}
|
|
14376
14476
|
function computeDeviceCapabilities(params) {
|
|
14377
14477
|
const { channel } = params;
|
|
@@ -14403,6 +14503,7 @@ function computeDeviceCapabilities(params) {
|
|
|
14403
14503
|
flat,
|
|
14404
14504
|
/white\s*led|whiteLed|flood\s*light|floodlight/i
|
|
14405
14505
|
);
|
|
14506
|
+
const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
|
|
14406
14507
|
const hasSirenFromAbilities = abilitiesHasAny(
|
|
14407
14508
|
flat,
|
|
14408
14509
|
/audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
|
|
@@ -14415,6 +14516,9 @@ function computeDeviceCapabilities(params) {
|
|
|
14415
14516
|
const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
|
|
14416
14517
|
const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
|
|
14417
14518
|
const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
|
|
14519
|
+
const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
|
|
14520
|
+
const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
|
|
14521
|
+
const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
|
|
14418
14522
|
const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
14419
14523
|
const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
14420
14524
|
const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
|
|
@@ -14430,14 +14534,15 @@ function computeDeviceCapabilities(params) {
|
|
|
14430
14534
|
hasZoom: finalHasZoom,
|
|
14431
14535
|
hasPresets: finalHasPresets,
|
|
14432
14536
|
hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
|
|
14433
|
-
hasBattery
|
|
14537
|
+
hasBattery,
|
|
14434
14538
|
hasIntercom: hasIntercomFromSupport,
|
|
14435
|
-
hasSiren: hasSirenFromAbilities,
|
|
14539
|
+
hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
|
|
14436
14540
|
// lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
|
|
14437
14541
|
hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
|
|
14438
14542
|
hasPir: hasPirFromAbilities || hasPirFromSupport,
|
|
14439
|
-
isDoorbell
|
|
14440
|
-
hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
|
|
14543
|
+
isDoorbell,
|
|
14544
|
+
hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
|
|
14545
|
+
hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
|
|
14441
14546
|
};
|
|
14442
14547
|
if (ptzMode !== void 0) result.ptzMode = ptzMode;
|
|
14443
14548
|
return result;
|
|
@@ -16102,6 +16207,162 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
|
|
|
16102
16207
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
16103
16208
|
init_recordingFileName();
|
|
16104
16209
|
|
|
16210
|
+
// src/reolink/baichuan/utils/chime.ts
|
|
16211
|
+
init_xml();
|
|
16212
|
+
var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16213
|
+
<body>
|
|
16214
|
+
<dingdongDeviceOpt version="1.1">
|
|
16215
|
+
<id>${chimeId}</id>
|
|
16216
|
+
<opt>getParam</opt>
|
|
16217
|
+
</dingdongDeviceOpt>
|
|
16218
|
+
</body>`;
|
|
16219
|
+
var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16220
|
+
<body>
|
|
16221
|
+
<dingdongDeviceOpt version="1.1">
|
|
16222
|
+
<opt>setParam</opt>
|
|
16223
|
+
<id>${chimeId}</id>
|
|
16224
|
+
${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
|
|
16225
|
+
${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
|
|
16226
|
+
${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
|
|
16227
|
+
</dingdongDeviceOpt>
|
|
16228
|
+
</body>`;
|
|
16229
|
+
var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16230
|
+
<body>
|
|
16231
|
+
<dingdongDeviceOpt version="1.1">
|
|
16232
|
+
<id>${chimeId}</id>
|
|
16233
|
+
<opt>ringWithMusic</opt>
|
|
16234
|
+
<musicId>${musicId}</musicId>
|
|
16235
|
+
</dingdongDeviceOpt>
|
|
16236
|
+
</body>`;
|
|
16237
|
+
var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16238
|
+
<body>
|
|
16239
|
+
<dingdongCfg version="1.1">
|
|
16240
|
+
<deviceCfg>
|
|
16241
|
+
<id>${chimeId}</id>
|
|
16242
|
+
<alarminCfg>
|
|
16243
|
+
<valid>${state}</valid>
|
|
16244
|
+
<musicId>${musicId}</musicId>
|
|
16245
|
+
<type>${eventType}</type>
|
|
16246
|
+
</alarminCfg>
|
|
16247
|
+
</deviceCfg>
|
|
16248
|
+
</dingdongCfg>
|
|
16249
|
+
</body>`;
|
|
16250
|
+
var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16251
|
+
<body>
|
|
16252
|
+
<dingdongCtrl version="1.1">
|
|
16253
|
+
<opt>machineStateGet</opt>
|
|
16254
|
+
</dingdongCtrl>
|
|
16255
|
+
</body>`;
|
|
16256
|
+
var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16257
|
+
<body>
|
|
16258
|
+
<dingdongCtrl version="1.1">
|
|
16259
|
+
<opt>machineStateSet</opt>
|
|
16260
|
+
<type>${chimeType}</type>
|
|
16261
|
+
<bopen>${enabled}</bopen>
|
|
16262
|
+
<bsave>1</bsave>
|
|
16263
|
+
<time>${time}</time>
|
|
16264
|
+
</dingdongCtrl>
|
|
16265
|
+
</body>`;
|
|
16266
|
+
var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16267
|
+
<body>
|
|
16268
|
+
<audioFileInfo version="1.1">
|
|
16269
|
+
<channelId>${channel}</channelId>
|
|
16270
|
+
<id>${fileId}</id>
|
|
16271
|
+
<timeout>0</timeout>
|
|
16272
|
+
</audioFileInfo>
|
|
16273
|
+
</body>`;
|
|
16274
|
+
var parseDingDongListFromXml = (xml) => {
|
|
16275
|
+
const devices = [];
|
|
16276
|
+
const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
|
|
16277
|
+
for (const block of blocks) {
|
|
16278
|
+
const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
|
|
16279
|
+
const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
|
|
16280
|
+
const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
|
|
16281
|
+
if (idText === void 0) continue;
|
|
16282
|
+
const id = Number(idText);
|
|
16283
|
+
if (!Number.isFinite(id)) continue;
|
|
16284
|
+
devices.push({
|
|
16285
|
+
id,
|
|
16286
|
+
name,
|
|
16287
|
+
netState: netStateText !== void 0 ? Number(netStateText) : 0
|
|
16288
|
+
});
|
|
16289
|
+
}
|
|
16290
|
+
return devices;
|
|
16291
|
+
};
|
|
16292
|
+
var parseDingDongParamsFromXml = (xml) => {
|
|
16293
|
+
const name = getXmlText(xml, "name");
|
|
16294
|
+
const volLevelText = getXmlText(xml, "volLevel");
|
|
16295
|
+
const ledStateText = getXmlText(xml, "ledState");
|
|
16296
|
+
const result = {};
|
|
16297
|
+
if (name !== void 0) result.name = name;
|
|
16298
|
+
if (volLevelText !== void 0) {
|
|
16299
|
+
const n = Number(volLevelText);
|
|
16300
|
+
if (Number.isFinite(n)) result.volLevel = n;
|
|
16301
|
+
}
|
|
16302
|
+
if (ledStateText !== void 0) {
|
|
16303
|
+
const n = Number(ledStateText);
|
|
16304
|
+
if (Number.isFinite(n)) result.ledState = n;
|
|
16305
|
+
}
|
|
16306
|
+
return result;
|
|
16307
|
+
};
|
|
16308
|
+
var parseDingDongCfgFromXml = (xml) => {
|
|
16309
|
+
const configs = [];
|
|
16310
|
+
const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
|
|
16311
|
+
for (const deviceBlock of deviceBlocks) {
|
|
16312
|
+
const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
|
|
16313
|
+
if (idText === void 0) continue;
|
|
16314
|
+
const id = Number(idText);
|
|
16315
|
+
if (!Number.isFinite(id)) continue;
|
|
16316
|
+
const typeMap = {};
|
|
16317
|
+
const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
|
|
16318
|
+
for (const alarmBlock of alarmBlocks) {
|
|
16319
|
+
const type = getXmlText(alarmBlock, "type");
|
|
16320
|
+
if (!type) continue;
|
|
16321
|
+
const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
|
|
16322
|
+
const musicIdText = getXmlText(alarmBlock, "musicId");
|
|
16323
|
+
typeMap[type] = {
|
|
16324
|
+
valid: validText !== void 0 ? Number(validText) : 0,
|
|
16325
|
+
musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
|
|
16326
|
+
};
|
|
16327
|
+
}
|
|
16328
|
+
configs.push({ id, type: typeMap });
|
|
16329
|
+
}
|
|
16330
|
+
return configs;
|
|
16331
|
+
};
|
|
16332
|
+
var parseHardwiredChimeFromXml = (xml) => {
|
|
16333
|
+
const type = getXmlText(xml, "type") ?? "";
|
|
16334
|
+
const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
|
|
16335
|
+
const timeText = getXmlText(xml, "time");
|
|
16336
|
+
return {
|
|
16337
|
+
type,
|
|
16338
|
+
enabled: bopenText === "1",
|
|
16339
|
+
time: timeText !== void 0 ? Number(timeText) : 0
|
|
16340
|
+
};
|
|
16341
|
+
};
|
|
16342
|
+
var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16343
|
+
<body>
|
|
16344
|
+
<dingdongSilentMode version="1.1">
|
|
16345
|
+
<id>${chimeId}</id>
|
|
16346
|
+
</dingdongSilentMode>
|
|
16347
|
+
</body>`;
|
|
16348
|
+
var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16349
|
+
<body>
|
|
16350
|
+
<dingdongSilentMode version="1.1">
|
|
16351
|
+
<id>${chimeId}</id>
|
|
16352
|
+
<time>${time}</time>
|
|
16353
|
+
<type>63</type>
|
|
16354
|
+
</dingdongSilentMode>
|
|
16355
|
+
</body>`;
|
|
16356
|
+
var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
|
|
16357
|
+
const timeText = getXmlText(xml, "time");
|
|
16358
|
+
const time = timeText !== void 0 ? Number(timeText) : 0;
|
|
16359
|
+
return {
|
|
16360
|
+
id: chimeId,
|
|
16361
|
+
time,
|
|
16362
|
+
active: time === 0
|
|
16363
|
+
};
|
|
16364
|
+
};
|
|
16365
|
+
|
|
16105
16366
|
// src/reolink/baichuan/utils/eventsGetEvents.ts
|
|
16106
16367
|
init_xml();
|
|
16107
16368
|
var parseAiTypeToken = (aiTypeRaw) => {
|
|
@@ -16414,6 +16675,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
16414
16675
|
host;
|
|
16415
16676
|
username;
|
|
16416
16677
|
password;
|
|
16678
|
+
/**
|
|
16679
|
+
* Set to `true` after `close()` is called.
|
|
16680
|
+
* Once closed, the API instance should not be reused.
|
|
16681
|
+
*/
|
|
16682
|
+
_closed = false;
|
|
16417
16683
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16418
16684
|
// SOCKET POOL - Tag-based socket management
|
|
16419
16685
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -16443,10 +16709,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
16443
16709
|
get client() {
|
|
16444
16710
|
const entry = this.socketPool.get("general");
|
|
16445
16711
|
if (!entry) {
|
|
16712
|
+
if (this._closed) {
|
|
16713
|
+
throw new Error(
|
|
16714
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
16715
|
+
);
|
|
16716
|
+
}
|
|
16446
16717
|
throw new Error("[ReolinkBaichuanApi] General socket not initialized");
|
|
16447
16718
|
}
|
|
16448
16719
|
return entry.client;
|
|
16449
16720
|
}
|
|
16721
|
+
/**
|
|
16722
|
+
* `true` after `close()` has been called. A closed API should not be reused;
|
|
16723
|
+
* the consumer should create a new instance.
|
|
16724
|
+
*/
|
|
16725
|
+
get isClosed() {
|
|
16726
|
+
return this._closed;
|
|
16727
|
+
}
|
|
16728
|
+
/**
|
|
16729
|
+
* `true` when the API is usable: not closed, general socket exists, socket
|
|
16730
|
+
* is connected and the client is logged in.
|
|
16731
|
+
*
|
|
16732
|
+
* This is the recommended way for consumers to check whether the API is
|
|
16733
|
+
* still valid before issuing commands, instead of directly accessing
|
|
16734
|
+
* `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
|
|
16735
|
+
* if the socket pool was already destroyed).
|
|
16736
|
+
*/
|
|
16737
|
+
get isReady() {
|
|
16738
|
+
if (this._closed) return false;
|
|
16739
|
+
const entry = this.socketPool.get("general");
|
|
16740
|
+
if (!entry) return false;
|
|
16741
|
+
try {
|
|
16742
|
+
return entry.client.isSocketConnected() && entry.client.loggedIn;
|
|
16743
|
+
} catch {
|
|
16744
|
+
return false;
|
|
16745
|
+
}
|
|
16746
|
+
}
|
|
16747
|
+
/** Promise tracking an in-flight reconnection from `ensureConnected()`. */
|
|
16748
|
+
_ensureConnectedPromise;
|
|
16749
|
+
/**
|
|
16750
|
+
* Ensure the "general" socket is connected and logged in.
|
|
16751
|
+
* If the socket is disconnected or the pool entry was destroyed, a new
|
|
16752
|
+
* general socket is created, logged in, and all event/push/guard listeners
|
|
16753
|
+
* are re-attached automatically.
|
|
16754
|
+
*
|
|
16755
|
+
* This is a **no-op** when the API is already {@link isReady}.
|
|
16756
|
+
*
|
|
16757
|
+
* @throws If `close()` was called — the API is permanently closed and a new
|
|
16758
|
+
* instance must be created.
|
|
16759
|
+
*/
|
|
16760
|
+
async ensureConnected() {
|
|
16761
|
+
if (this._closed) {
|
|
16762
|
+
throw new Error(
|
|
16763
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
16764
|
+
);
|
|
16765
|
+
}
|
|
16766
|
+
if (this.isReady) return;
|
|
16767
|
+
if (this._ensureConnectedPromise) {
|
|
16768
|
+
return this._ensureConnectedPromise;
|
|
16769
|
+
}
|
|
16770
|
+
this._ensureConnectedPromise = this.reconnectGeneralSocket();
|
|
16771
|
+
try {
|
|
16772
|
+
await this._ensureConnectedPromise;
|
|
16773
|
+
} finally {
|
|
16774
|
+
this._ensureConnectedPromise = void 0;
|
|
16775
|
+
}
|
|
16776
|
+
}
|
|
16777
|
+
/**
|
|
16778
|
+
* Internal: destroy the current general socket (if any), create a new one,
|
|
16779
|
+
* login, and re-attach all listeners.
|
|
16780
|
+
*/
|
|
16781
|
+
async reconnectGeneralSocket() {
|
|
16782
|
+
const oldEntry = this.socketPool.get("general");
|
|
16783
|
+
if (oldEntry) {
|
|
16784
|
+
oldEntry.client.removeAllListeners();
|
|
16785
|
+
if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
|
|
16786
|
+
if (oldEntry.generalPermitRelease) {
|
|
16787
|
+
try {
|
|
16788
|
+
oldEntry.generalPermitRelease();
|
|
16789
|
+
} catch {
|
|
16790
|
+
}
|
|
16791
|
+
}
|
|
16792
|
+
this.socketPool.delete("general");
|
|
16793
|
+
try {
|
|
16794
|
+
await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
|
|
16795
|
+
} catch {
|
|
16796
|
+
}
|
|
16797
|
+
}
|
|
16798
|
+
const newClient = new BaichuanClient(this.clientOptions);
|
|
16799
|
+
this.socketPool.set("general", {
|
|
16800
|
+
client: newClient,
|
|
16801
|
+
refCount: 1,
|
|
16802
|
+
// general socket is always "in use"
|
|
16803
|
+
createdAt: Date.now(),
|
|
16804
|
+
lastUsedAt: Date.now(),
|
|
16805
|
+
idleCloseTimer: void 0,
|
|
16806
|
+
generalPermitRelease: void 0
|
|
16807
|
+
});
|
|
16808
|
+
this.setupGeneralClientListeners();
|
|
16809
|
+
await this.client.login();
|
|
16810
|
+
this.logger.log?.(
|
|
16811
|
+
"[ReolinkBaichuanApi] General socket reconnected successfully"
|
|
16812
|
+
);
|
|
16813
|
+
if (this.simpleEventListeners.size > 0) {
|
|
16814
|
+
this.simpleEventSubscribed = false;
|
|
16815
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
16816
|
+
this.simpleEventWatchdogLastRecoveryAt = 0;
|
|
16817
|
+
try {
|
|
16818
|
+
await this.ensureSimpleEventSubscribed();
|
|
16819
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
16820
|
+
this.logger.log?.(
|
|
16821
|
+
`[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
|
|
16822
|
+
);
|
|
16823
|
+
} catch (e) {
|
|
16824
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
16825
|
+
this.logger,
|
|
16826
|
+
`[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
|
|
16827
|
+
formatErrorForLog(e)
|
|
16828
|
+
);
|
|
16829
|
+
}
|
|
16830
|
+
}
|
|
16831
|
+
}
|
|
16832
|
+
/**
|
|
16833
|
+
* Attach event, push, channelInfo, and guard listeners to the current
|
|
16834
|
+
* "general" client. Called from the constructor and from
|
|
16835
|
+
* {@link reconnectGeneralSocket}.
|
|
16836
|
+
*/
|
|
16837
|
+
setupGeneralClientListeners() {
|
|
16838
|
+
const client = this.client;
|
|
16839
|
+
client.on("event", (event) => {
|
|
16840
|
+
const mapped = mapToSimpleEvent(event);
|
|
16841
|
+
if (!mapped) return;
|
|
16842
|
+
this.dispatchSimpleEvent(mapped);
|
|
16843
|
+
});
|
|
16844
|
+
client.on("channelInfo", (xml) => {
|
|
16845
|
+
try {
|
|
16846
|
+
this.parseAndStoreChannelInfo(xml);
|
|
16847
|
+
} catch (e) {
|
|
16848
|
+
this.logger.warn?.(
|
|
16849
|
+
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
16850
|
+
formatErrorForLog(e)
|
|
16851
|
+
);
|
|
16852
|
+
}
|
|
16853
|
+
});
|
|
16854
|
+
client.on("push", (frame) => {
|
|
16855
|
+
const cmdId = frame.header.cmdId;
|
|
16856
|
+
if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
|
|
16857
|
+
return;
|
|
16858
|
+
}
|
|
16859
|
+
try {
|
|
16860
|
+
if (frame.body.length === 0) return;
|
|
16861
|
+
const xml = client.tryDecryptXml(
|
|
16862
|
+
frame.body,
|
|
16863
|
+
frame.header.channelId,
|
|
16864
|
+
client.enc
|
|
16865
|
+
);
|
|
16866
|
+
if (!xml || !xml.startsWith("<?xml")) return;
|
|
16867
|
+
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
16868
|
+
} catch (e) {
|
|
16869
|
+
this.logger.debug?.(
|
|
16870
|
+
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
16871
|
+
formatErrorForLog(e)
|
|
16872
|
+
);
|
|
16873
|
+
}
|
|
16874
|
+
});
|
|
16875
|
+
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
16876
|
+
client.on("close", () => {
|
|
16877
|
+
try {
|
|
16878
|
+
void this.maybeRebootOnDisconnectStorm();
|
|
16879
|
+
} catch {
|
|
16880
|
+
}
|
|
16881
|
+
});
|
|
16882
|
+
}
|
|
16883
|
+
if (this.rebootAfterConsecutiveEconnreset > 0) {
|
|
16884
|
+
client.on("close", () => {
|
|
16885
|
+
try {
|
|
16886
|
+
void this.maybeRebootOnEconnresetStorm();
|
|
16887
|
+
} catch {
|
|
16888
|
+
}
|
|
16889
|
+
});
|
|
16890
|
+
}
|
|
16891
|
+
if (!this.sessionGuardIntervalTimer) {
|
|
16892
|
+
client.once("push", () => {
|
|
16893
|
+
void this.logActiveSessionsOnStartup();
|
|
16894
|
+
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
16895
|
+
void this.maybeRebootOnTooManySessions();
|
|
16896
|
+
}, 6e4);
|
|
16897
|
+
});
|
|
16898
|
+
}
|
|
16899
|
+
}
|
|
16450
16900
|
/**
|
|
16451
16901
|
* Cached camera UID. May be initially undefined if not provided in the constructor.
|
|
16452
16902
|
* Will be lazily populated on demand when needed (e.g. for recordings).
|
|
@@ -17021,6 +17471,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17021
17471
|
`[SocketPool] Closing existing socket for tag=${tag} (recreating)`
|
|
17022
17472
|
);
|
|
17023
17473
|
this.socketPool.delete(tag);
|
|
17474
|
+
if (existing.generalPermitRelease) {
|
|
17475
|
+
try {
|
|
17476
|
+
existing.generalPermitRelease();
|
|
17477
|
+
} catch {
|
|
17478
|
+
}
|
|
17479
|
+
existing.generalPermitRelease = void 0;
|
|
17480
|
+
}
|
|
17024
17481
|
try {
|
|
17025
17482
|
await existing.client.close({
|
|
17026
17483
|
reason: "socket pool recreation",
|
|
@@ -17039,7 +17496,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17039
17496
|
refCount: 0,
|
|
17040
17497
|
createdAt: now,
|
|
17041
17498
|
lastUsedAt: now,
|
|
17042
|
-
idleCloseTimer: void 0
|
|
17499
|
+
idleCloseTimer: void 0,
|
|
17500
|
+
generalPermitRelease: void 0
|
|
17043
17501
|
};
|
|
17044
17502
|
entry.pendingPromise = (async () => {
|
|
17045
17503
|
try {
|
|
@@ -17057,6 +17515,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17057
17515
|
entry.lastUsedAt = Date.now();
|
|
17058
17516
|
delete entry.pendingPromise;
|
|
17059
17517
|
log?.log?.(`[SocketPool] Socket connected for tag=${tag}`);
|
|
17518
|
+
if (tag !== "general") {
|
|
17519
|
+
try {
|
|
17520
|
+
const generalEntry = this.socketPool.get("general");
|
|
17521
|
+
if (generalEntry?.client) {
|
|
17522
|
+
entry.generalPermitRelease = generalEntry.client.acquirePermit(
|
|
17523
|
+
0,
|
|
17524
|
+
// indefinite — released when the streaming socket closes
|
|
17525
|
+
`streaming-peer:${tag}`
|
|
17526
|
+
);
|
|
17527
|
+
}
|
|
17528
|
+
} catch {
|
|
17529
|
+
}
|
|
17530
|
+
}
|
|
17060
17531
|
void this.maybeRebootOnTooManySessions();
|
|
17061
17532
|
return newClient;
|
|
17062
17533
|
} catch (loginError) {
|
|
@@ -17125,6 +17596,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17125
17596
|
if (!current) return;
|
|
17126
17597
|
if (current.refCount > 0) return;
|
|
17127
17598
|
this.socketPool.delete(tag);
|
|
17599
|
+
if (current.generalPermitRelease) {
|
|
17600
|
+
try {
|
|
17601
|
+
current.generalPermitRelease();
|
|
17602
|
+
} catch {
|
|
17603
|
+
}
|
|
17604
|
+
current.generalPermitRelease = void 0;
|
|
17605
|
+
}
|
|
17128
17606
|
log?.log?.(`[SocketPool] Closing idle streaming socket for tag=${tag}`);
|
|
17129
17607
|
try {
|
|
17130
17608
|
await current.client.close({
|
|
@@ -17179,6 +17657,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17179
17657
|
clearTimeout(entry.idleCloseTimer);
|
|
17180
17658
|
entry.idleCloseTimer = void 0;
|
|
17181
17659
|
}
|
|
17660
|
+
if (entry.generalPermitRelease) {
|
|
17661
|
+
try {
|
|
17662
|
+
entry.generalPermitRelease();
|
|
17663
|
+
} catch {
|
|
17664
|
+
}
|
|
17665
|
+
entry.generalPermitRelease = void 0;
|
|
17666
|
+
}
|
|
17182
17667
|
log?.debug?.(`[SocketPool] Force-closing socket for tag=${tag}`);
|
|
17183
17668
|
this.socketPool.delete(tag);
|
|
17184
17669
|
try {
|
|
@@ -17204,6 +17689,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17204
17689
|
if (entry.idleCloseTimer) {
|
|
17205
17690
|
clearTimeout(entry.idleCloseTimer);
|
|
17206
17691
|
}
|
|
17692
|
+
if (entry.generalPermitRelease) {
|
|
17693
|
+
try {
|
|
17694
|
+
entry.generalPermitRelease();
|
|
17695
|
+
} catch {
|
|
17696
|
+
}
|
|
17697
|
+
entry.generalPermitRelease = void 0;
|
|
17698
|
+
}
|
|
17207
17699
|
this.logger?.debug?.(`[SocketPool] Cleanup: closing tag=${tag}`);
|
|
17208
17700
|
await entry.client.close({ reason: "API cleanup", skipLogout: true });
|
|
17209
17701
|
} catch {
|
|
@@ -17309,7 +17801,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17309
17801
|
password: opts.password,
|
|
17310
17802
|
...opts.logger ? { logger: opts.logger } : {},
|
|
17311
17803
|
...opts.debugOptions ? { debugOptions: opts.debugOptions } : {},
|
|
17312
|
-
...opts.uid ? { uid: opts.uid } : {}
|
|
17804
|
+
...opts.uid ? { uid: opts.uid } : {},
|
|
17805
|
+
...opts.transport ? { transport: opts.transport } : {},
|
|
17806
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
17807
|
+
...opts.udpDiscoveryMethod ? { udpDiscoveryMethod: opts.udpDiscoveryMethod } : {},
|
|
17808
|
+
...opts.idleDisconnect !== void 0 ? { idleDisconnect: opts.idleDisconnect } : {},
|
|
17809
|
+
...opts.idleDisconnectTimeoutMs !== void 0 ? { idleDisconnectTimeoutMs: opts.idleDisconnectTimeoutMs } : {},
|
|
17810
|
+
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
17313
17811
|
};
|
|
17314
17812
|
const generalClient = new BaichuanClient(opts);
|
|
17315
17813
|
this.socketPool.set("general", {
|
|
@@ -17318,7 +17816,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17318
17816
|
// Always keep general socket "in use"
|
|
17319
17817
|
createdAt: Date.now(),
|
|
17320
17818
|
lastUsedAt: Date.now(),
|
|
17321
|
-
idleCloseTimer: void 0
|
|
17819
|
+
idleCloseTimer: void 0,
|
|
17820
|
+
generalPermitRelease: void 0
|
|
17322
17821
|
});
|
|
17323
17822
|
this.host = opts.host;
|
|
17324
17823
|
this.username = opts.username;
|
|
@@ -17338,42 +17837,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17338
17837
|
logger: this.logger,
|
|
17339
17838
|
debugConfig: generalClient.getDebugConfig?.()
|
|
17340
17839
|
});
|
|
17341
|
-
this.client.on("event", (event) => {
|
|
17342
|
-
const mapped = mapToSimpleEvent(event);
|
|
17343
|
-
if (!mapped) return;
|
|
17344
|
-
this.dispatchSimpleEvent(mapped);
|
|
17345
|
-
});
|
|
17346
|
-
this.client.on("channelInfo", (xml) => {
|
|
17347
|
-
try {
|
|
17348
|
-
this.parseAndStoreChannelInfo(xml);
|
|
17349
|
-
} catch (e) {
|
|
17350
|
-
this.logger.warn?.(
|
|
17351
|
-
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
17352
|
-
formatErrorForLog(e)
|
|
17353
|
-
);
|
|
17354
|
-
}
|
|
17355
|
-
});
|
|
17356
|
-
this.client.on("push", (frame) => {
|
|
17357
|
-
const cmdId = frame.header.cmdId;
|
|
17358
|
-
if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
|
|
17359
|
-
return;
|
|
17360
|
-
}
|
|
17361
|
-
try {
|
|
17362
|
-
if (frame.body.length === 0) return;
|
|
17363
|
-
const xml = this.client.tryDecryptXml(
|
|
17364
|
-
frame.body,
|
|
17365
|
-
frame.header.channelId,
|
|
17366
|
-
this.client.enc
|
|
17367
|
-
);
|
|
17368
|
-
if (!xml || !xml.startsWith("<?xml")) return;
|
|
17369
|
-
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
17370
|
-
} catch (e) {
|
|
17371
|
-
this.logger.debug?.(
|
|
17372
|
-
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
17373
|
-
formatErrorForLog(e)
|
|
17374
|
-
);
|
|
17375
|
-
}
|
|
17376
|
-
});
|
|
17377
17840
|
const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
|
|
17378
17841
|
if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
|
|
17379
17842
|
this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
|
|
@@ -17382,32 +17845,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17382
17845
|
if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
|
|
17383
17846
|
this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
|
|
17384
17847
|
}
|
|
17385
|
-
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
17386
|
-
this.client.on("close", () => {
|
|
17387
|
-
try {
|
|
17388
|
-
void this.maybeRebootOnDisconnectStorm();
|
|
17389
|
-
} catch {
|
|
17390
|
-
}
|
|
17391
|
-
});
|
|
17392
|
-
}
|
|
17393
17848
|
const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
|
|
17394
17849
|
if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
|
|
17395
17850
|
this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
|
|
17396
17851
|
}
|
|
17397
|
-
|
|
17398
|
-
this.client.on("close", () => {
|
|
17399
|
-
try {
|
|
17400
|
-
void this.maybeRebootOnEconnresetStorm();
|
|
17401
|
-
} catch {
|
|
17402
|
-
}
|
|
17403
|
-
});
|
|
17404
|
-
}
|
|
17405
|
-
this.client.once("push", () => {
|
|
17406
|
-
void this.logActiveSessionsOnStartup();
|
|
17407
|
-
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
17408
|
-
void this.maybeRebootOnTooManySessions();
|
|
17409
|
-
}, 6e4);
|
|
17410
|
-
});
|
|
17852
|
+
this.setupGeneralClientListeners();
|
|
17411
17853
|
}
|
|
17412
17854
|
/**
|
|
17413
17855
|
* CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
|
|
@@ -17821,7 +18263,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17821
18263
|
*/
|
|
17822
18264
|
async onSimpleEvent(callback) {
|
|
17823
18265
|
this.simpleEventListeners.add(callback);
|
|
17824
|
-
|
|
18266
|
+
this.logger.debug?.(
|
|
18267
|
+
`[ReolinkBaichuanApi] onSimpleEvent: registering listener (total=${this.simpleEventListeners.size})`
|
|
18268
|
+
);
|
|
18269
|
+
try {
|
|
18270
|
+
await this.ensureSimpleEventSubscribed();
|
|
18271
|
+
this.logger.debug?.(
|
|
18272
|
+
`[ReolinkBaichuanApi] onSimpleEvent: initial subscribe succeeded, simpleEventSubscribed=${this.simpleEventSubscribed}`
|
|
18273
|
+
);
|
|
18274
|
+
} catch (e) {
|
|
18275
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
18276
|
+
this.logger,
|
|
18277
|
+
`[ReolinkBaichuanApi] onSimpleEvent: initial subscribe failed, simpleEventSubscribed=${this.simpleEventSubscribed}, watchdog will retry`,
|
|
18278
|
+
formatErrorForLog(e)
|
|
18279
|
+
);
|
|
18280
|
+
}
|
|
17825
18281
|
this.simpleEventLastReceivedAt = Date.now();
|
|
17826
18282
|
this.startSimpleEventResubscribeTimer();
|
|
17827
18283
|
this.startSimpleEventWatchdog();
|
|
@@ -17842,11 +18298,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17842
18298
|
this.stopUdpSleepInference();
|
|
17843
18299
|
await this.ensureSimpleEventUnsubscribed();
|
|
17844
18300
|
} else {
|
|
17845
|
-
const
|
|
17846
|
-
if (
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
18301
|
+
const generalEntry = this.socketPool.get("general");
|
|
18302
|
+
if (generalEntry) {
|
|
18303
|
+
const isUdp = generalEntry.client.getTransport?.() === "udp";
|
|
18304
|
+
if (isUdp) {
|
|
18305
|
+
this.startUdpSleepInference();
|
|
18306
|
+
} else if (generalEntry.client.isStatePollingEnabled?.()) {
|
|
18307
|
+
this.startStatePolling();
|
|
18308
|
+
}
|
|
17850
18309
|
}
|
|
17851
18310
|
}
|
|
17852
18311
|
}
|
|
@@ -17888,8 +18347,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17888
18347
|
}
|
|
17889
18348
|
async simpleEventWatchdogTick() {
|
|
17890
18349
|
if (this.simpleEventListeners.size === 0) return;
|
|
17891
|
-
|
|
18350
|
+
const generalEntry = this.socketPool.get("general");
|
|
18351
|
+
if (!generalEntry) return;
|
|
18352
|
+
if (!generalEntry.client.isSocketConnected?.() || !generalEntry.client.loggedIn) {
|
|
18353
|
+
this.logger.debug?.(
|
|
18354
|
+
`[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
|
|
18355
|
+
);
|
|
18356
|
+
return;
|
|
18357
|
+
}
|
|
17892
18358
|
const now = Date.now();
|
|
18359
|
+
const sinceLastEvent = this.simpleEventLastReceivedAt > 0 ? now - this.simpleEventLastReceivedAt : -1;
|
|
18360
|
+
this.logger.debug?.(
|
|
18361
|
+
`[ReolinkBaichuanApi] event watchdog tick: subscribed=${this.simpleEventSubscribed} clientSubscribed=${generalEntry.client.subscribed} lastEventAgoMs=${sinceLastEvent} recoveryAttempts=${this.simpleEventWatchdogRecoveryAttempts} listeners=${this.simpleEventListeners.size}`
|
|
18362
|
+
);
|
|
17893
18363
|
if (this.simpleEventSubscribed && this.simpleEventLastReceivedAt > 0) {
|
|
17894
18364
|
const silence = now - this.simpleEventLastReceivedAt;
|
|
17895
18365
|
if (silence < this.simpleEventWatchdogSilenceThresholdMs) return;
|
|
@@ -17900,7 +18370,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17900
18370
|
);
|
|
17901
18371
|
try {
|
|
17902
18372
|
this.simpleEventSubscribed = false;
|
|
17903
|
-
|
|
18373
|
+
generalEntry.client.subscribed = false;
|
|
17904
18374
|
await this.ensureSimpleEventSubscribed();
|
|
17905
18375
|
this.simpleEventLastReceivedAt = Date.now();
|
|
17906
18376
|
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
@@ -17918,6 +18388,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17918
18388
|
return;
|
|
17919
18389
|
}
|
|
17920
18390
|
if (!this.simpleEventSubscribed) {
|
|
18391
|
+
if (this.simpleEventLastReceivedAt > 0) {
|
|
18392
|
+
const sinceLastEvent2 = now - this.simpleEventLastReceivedAt;
|
|
18393
|
+
if (sinceLastEvent2 < this.simpleEventWatchdogSilenceThresholdMs) {
|
|
18394
|
+
this.simpleEventSubscribed = true;
|
|
18395
|
+
this.logger.debug?.(
|
|
18396
|
+
`[ReolinkBaichuanApi] event watchdog: events flowing (lastEventAgo=${Math.round(sinceLastEvent2 / 1e3)}s) despite simpleEventSubscribed=false, marking subscription as active (recoveryAttempts=${this.simpleEventWatchdogRecoveryAttempts})`
|
|
18397
|
+
);
|
|
18398
|
+
if (this.simpleEventWatchdogRecoveryAttempts > 0) {
|
|
18399
|
+
(this.logger.info ?? this.logger.log).call(
|
|
18400
|
+
this.logger,
|
|
18401
|
+
`[ReolinkBaichuanApi] event watchdog: events flowing despite failed subscribe, marking subscription active`
|
|
18402
|
+
);
|
|
18403
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
18404
|
+
}
|
|
18405
|
+
return;
|
|
18406
|
+
} else {
|
|
18407
|
+
this.logger.debug?.(
|
|
18408
|
+
`[ReolinkBaichuanApi] event watchdog: events stale (lastEventAgo=${Math.round(sinceLastEvent2 / 1e3)}s, threshold=${Math.round(this.simpleEventWatchdogSilenceThresholdMs / 1e3)}s), proceeding with recovery`
|
|
18409
|
+
);
|
|
18410
|
+
}
|
|
18411
|
+
} else {
|
|
18412
|
+
this.logger.debug?.(
|
|
18413
|
+
`[ReolinkBaichuanApi] event watchdog: no events ever received (simpleEventLastReceivedAt=0), proceeding with recovery`
|
|
18414
|
+
);
|
|
18415
|
+
}
|
|
17921
18416
|
const backoffMs = Math.min(
|
|
17922
18417
|
3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
|
|
17923
18418
|
this.simpleEventWatchdogSilenceThresholdMs
|
|
@@ -17981,20 +18476,51 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17981
18476
|
return await this.simpleEventResubscribeInFlight;
|
|
17982
18477
|
}
|
|
17983
18478
|
async ensureSimpleEventSubscribed() {
|
|
17984
|
-
if (this.simpleEventListeners.size === 0)
|
|
17985
|
-
|
|
17986
|
-
|
|
18479
|
+
if (this.simpleEventListeners.size === 0) {
|
|
18480
|
+
this.logger.debug?.(
|
|
18481
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: no listeners, skipping`
|
|
18482
|
+
);
|
|
18483
|
+
return;
|
|
18484
|
+
}
|
|
18485
|
+
if (this.simpleEventSubscribed) {
|
|
18486
|
+
this.logger.debug?.(
|
|
18487
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: already subscribed, skipping`
|
|
18488
|
+
);
|
|
18489
|
+
return;
|
|
18490
|
+
}
|
|
18491
|
+
if (this.simpleEventSubscribeInFlight) {
|
|
18492
|
+
this.logger.debug?.(
|
|
18493
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: subscribe already in-flight, awaiting`
|
|
18494
|
+
);
|
|
17987
18495
|
return await this.simpleEventSubscribeInFlight;
|
|
18496
|
+
}
|
|
18497
|
+
this.logger.debug?.(
|
|
18498
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: starting subscribe (clientSubscribed=${this.socketPool.get("general")?.client.subscribed})`
|
|
18499
|
+
);
|
|
17988
18500
|
this.simpleEventSubscribeInFlight = (async () => {
|
|
17989
|
-
|
|
18501
|
+
const entry = this.socketPool.get("general");
|
|
18502
|
+
if (!entry) {
|
|
18503
|
+
this.logger.debug?.(
|
|
18504
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: no general socket, bailing out`
|
|
18505
|
+
);
|
|
18506
|
+
return;
|
|
18507
|
+
}
|
|
18508
|
+
if (!entry.client.subscribed) {
|
|
18509
|
+
this.logger.debug?.(
|
|
18510
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: client.subscribed=false, calling subscribeEvents()`
|
|
18511
|
+
);
|
|
17990
18512
|
await this.subscribeEvents();
|
|
18513
|
+
} else {
|
|
18514
|
+
this.logger.debug?.(
|
|
18515
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: client already subscribed, skipping subscribeEvents()`
|
|
18516
|
+
);
|
|
17991
18517
|
}
|
|
17992
18518
|
this.simpleEventSubscribed = true;
|
|
17993
|
-
const isUdp =
|
|
18519
|
+
const isUdp = entry.client.getTransport?.() === "udp";
|
|
17994
18520
|
if (isUdp) {
|
|
17995
18521
|
this.startUdpSleepInference();
|
|
17996
|
-
} else if (
|
|
17997
|
-
const channel =
|
|
18522
|
+
} else if (entry.client.isStatePollingEnabled?.()) {
|
|
18523
|
+
const channel = entry.client.getConfiguredChannel?.() ?? 0;
|
|
17998
18524
|
await this.checkAndDispatchCurrentState(channel);
|
|
17999
18525
|
this.startStatePolling();
|
|
18000
18526
|
}
|
|
@@ -18004,7 +18530,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18004
18530
|
return await this.simpleEventSubscribeInFlight;
|
|
18005
18531
|
}
|
|
18006
18532
|
async ensureSimpleEventUnsubscribed() {
|
|
18007
|
-
|
|
18533
|
+
const generalEntry = this.socketPool.get("general");
|
|
18534
|
+
if (!generalEntry) {
|
|
18535
|
+
this.simpleEventSubscribed = false;
|
|
18536
|
+
this.stopSimpleEventResubscribeTimer();
|
|
18537
|
+
this.stopStatePolling();
|
|
18538
|
+
this.stopUdpSleepInference();
|
|
18539
|
+
return;
|
|
18540
|
+
}
|
|
18541
|
+
if (!this.simpleEventSubscribed && !generalEntry.client.subscribed) return;
|
|
18008
18542
|
if (this.simpleEventUnsubscribeInFlight)
|
|
18009
18543
|
return await this.simpleEventUnsubscribeInFlight;
|
|
18010
18544
|
if (this.simpleEventSubscribeInFlight) {
|
|
@@ -18146,6 +18680,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18146
18680
|
);
|
|
18147
18681
|
}
|
|
18148
18682
|
async close(options) {
|
|
18683
|
+
if (this._closed) return;
|
|
18684
|
+
this._closed = true;
|
|
18149
18685
|
if (this.sessionGuardIntervalTimer) {
|
|
18150
18686
|
clearInterval(this.sessionGuardIntervalTimer);
|
|
18151
18687
|
this.sessionGuardIntervalTimer = void 0;
|
|
@@ -18208,7 +18744,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18208
18744
|
}
|
|
18209
18745
|
async handleSendXml400(params, frame, retry) {
|
|
18210
18746
|
const emptyBody = frame.body.length === 0;
|
|
18211
|
-
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes:
|
|
18747
|
+
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
|
|
18212
18748
|
if (this.isSendXmlFailFast400(params, frame.body.length)) {
|
|
18213
18749
|
throw new Error(emptyBody400Msg);
|
|
18214
18750
|
}
|
|
@@ -18724,11 +19260,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18724
19260
|
* Minimal per-channel inventory for NVR-connected devices.
|
|
18725
19261
|
*
|
|
18726
19262
|
* Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
|
|
19263
|
+
*
|
|
19264
|
+
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
19265
|
+
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
19266
|
+
* no dependency on async push messages. Recommended for first-call discovery.
|
|
19267
|
+
* - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
|
|
19268
|
+
* info after login + event subscription. This push is *asynchronous*: if it has not
|
|
19269
|
+
* arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
|
|
19270
|
+
* with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
|
|
18727
19271
|
*/
|
|
18728
19272
|
async getNvrChannelsSummary(options) {
|
|
18729
|
-
const source = options?.source ?? "
|
|
18730
|
-
|
|
18731
|
-
const
|
|
19273
|
+
const source = options?.source ?? "cgi";
|
|
19274
|
+
let channels;
|
|
19275
|
+
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
19276
|
+
if (options?.channels?.length) {
|
|
19277
|
+
channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
19278
|
+
} else if (source === "cgi") {
|
|
19279
|
+
try {
|
|
19280
|
+
const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
|
|
19281
|
+
const status = channelsResponse?.[0]?.value?.status ?? [];
|
|
19282
|
+
for (const s of status) {
|
|
19283
|
+
const ch = Number(s?.channel);
|
|
19284
|
+
if (!Number.isFinite(ch)) continue;
|
|
19285
|
+
cgiStatusByChannel.set(ch, {
|
|
19286
|
+
...s.name != null ? { name: s.name } : {},
|
|
19287
|
+
...s.uid != null ? { uid: s.uid } : {},
|
|
19288
|
+
sleeping: s.sleep === 1
|
|
19289
|
+
});
|
|
19290
|
+
}
|
|
19291
|
+
channels = cgiChannels;
|
|
19292
|
+
this.logger.debug?.(
|
|
19293
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
19294
|
+
);
|
|
19295
|
+
} catch (e) {
|
|
19296
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19297
|
+
this.logger.warn?.(
|
|
19298
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
|
|
19299
|
+
);
|
|
19300
|
+
channels = [];
|
|
19301
|
+
}
|
|
19302
|
+
} else {
|
|
19303
|
+
const pushInfo2 = this.getChannelInfoFromPushCache();
|
|
19304
|
+
channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
19305
|
+
}
|
|
19306
|
+
channels = channels.sort((a, b) => a - b);
|
|
18732
19307
|
const support = await this.getSupportInfo().catch(() => {
|
|
18733
19308
|
this.logger.error?.(
|
|
18734
19309
|
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
@@ -18758,7 +19333,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18758
19333
|
);
|
|
18759
19334
|
}
|
|
18760
19335
|
}
|
|
18761
|
-
const cacheKey =
|
|
19336
|
+
const cacheKey = `${source}:${channels.join(",")}`;
|
|
18762
19337
|
const cached = this.nvrChannelsSummaryCache.get(cacheKey);
|
|
18763
19338
|
if (cached) {
|
|
18764
19339
|
return {
|
|
@@ -18779,8 +19354,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18779
19354
|
} catch {
|
|
18780
19355
|
}
|
|
18781
19356
|
}
|
|
19357
|
+
const pushInfo = this.getChannelInfoFromPushCache();
|
|
18782
19358
|
const devices = channels.map((channel) => {
|
|
18783
|
-
const
|
|
19359
|
+
const pushCached = pushInfo.get(channel);
|
|
19360
|
+
const cgiStatus = cgiStatusByChannel.get(channel);
|
|
18784
19361
|
const info = infoPerChannel.get(channel);
|
|
18785
19362
|
const networkInfo = networkInfoPerChannel.get(channel);
|
|
18786
19363
|
const isBattery = isBatteryByChannel.get(channel) ?? false;
|
|
@@ -18788,6 +19365,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18788
19365
|
const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
|
|
18789
19366
|
const normalizedModel = model ? model.trim() : void 0;
|
|
18790
19367
|
const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
19368
|
+
const name = pushCached?.name || cgiStatus?.name || "";
|
|
19369
|
+
const uid = pushCached?.uid || cgiStatus?.uid || "";
|
|
19370
|
+
const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
|
|
18791
19371
|
return {
|
|
18792
19372
|
channel,
|
|
18793
19373
|
isBattery,
|
|
@@ -18797,19 +19377,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18797
19377
|
...networkInfo?.ip ? { ip: networkInfo.ip } : {},
|
|
18798
19378
|
...networkInfo?.mac ? { mac: networkInfo.mac } : {},
|
|
18799
19379
|
...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
|
|
18800
|
-
...
|
|
18801
|
-
...
|
|
18802
|
-
...
|
|
18803
|
-
...typeof
|
|
18804
|
-
...
|
|
18805
|
-
...
|
|
18806
|
-
...
|
|
18807
|
-
...typeof
|
|
18808
|
-
...typeof
|
|
18809
|
-
...typeof
|
|
18810
|
-
...typeof
|
|
18811
|
-
...
|
|
18812
|
-
...typeof
|
|
19380
|
+
...name ? { name } : {},
|
|
19381
|
+
...uid ? { uid } : {},
|
|
19382
|
+
...pushCached?.state ? { state: pushCached.state } : {},
|
|
19383
|
+
...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
|
|
19384
|
+
...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
|
|
19385
|
+
...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
|
|
19386
|
+
...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
|
|
19387
|
+
...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
|
|
19388
|
+
...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
|
|
19389
|
+
...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
|
|
19390
|
+
...typeof sleeping === "boolean" ? { sleeping } : {},
|
|
19391
|
+
...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
|
|
19392
|
+
...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
|
|
18813
19393
|
};
|
|
18814
19394
|
});
|
|
18815
19395
|
const result = { channels, devices };
|
|
@@ -19989,6 +20569,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19989
20569
|
this._processVideoclipThumbnailQueue();
|
|
19990
20570
|
}
|
|
19991
20571
|
}
|
|
20572
|
+
if (this.videoclipThumbnailQueue.length >= 50) {
|
|
20573
|
+
throw new Error(
|
|
20574
|
+
`Thumbnail queue full (${this.videoclipThumbnailQueue.length}/50) \u2013 request rejected to protect camera stability`
|
|
20575
|
+
);
|
|
20576
|
+
}
|
|
19992
20577
|
return new Promise((resolve, reject) => {
|
|
19993
20578
|
this.videoclipThumbnailQueue.push({ params, resolve, reject });
|
|
19994
20579
|
});
|
|
@@ -20089,10 +20674,7 @@ ${xml}`);
|
|
|
20089
20674
|
messageClass: BC_CLASS_MODERN_24,
|
|
20090
20675
|
streamType: 0,
|
|
20091
20676
|
payloadXml: xml,
|
|
20092
|
-
timeoutMs
|
|
20093
|
-
// Retry parameters - camera often rejects first few requests
|
|
20094
|
-
maxRetries: 8,
|
|
20095
|
-
retryDelayMs: 1500
|
|
20677
|
+
timeoutMs
|
|
20096
20678
|
});
|
|
20097
20679
|
trace(`CoverPreview succeeded`);
|
|
20098
20680
|
} catch (e) {
|
|
@@ -21485,8 +22067,10 @@ ${stderr}`)
|
|
|
21485
22067
|
* Unsubscribe from events.
|
|
21486
22068
|
*/
|
|
21487
22069
|
async unsubscribeEvents() {
|
|
21488
|
-
this.
|
|
21489
|
-
|
|
22070
|
+
const generalEntry = this.socketPool.get("general");
|
|
22071
|
+
if (!generalEntry) return;
|
|
22072
|
+
generalEntry.client.subscribed = false;
|
|
22073
|
+
generalEntry.client.refreshKeepAlive?.();
|
|
21490
22074
|
}
|
|
21491
22075
|
/**
|
|
21492
22076
|
* Check current motion and AI state and dispatch events if state changed.
|
|
@@ -23073,13 +23657,12 @@ ${xml}`
|
|
|
23073
23657
|
]);
|
|
23074
23658
|
const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
|
|
23075
23659
|
const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
|
|
23076
|
-
const supportItem =
|
|
23077
|
-
const capabilities =
|
|
23078
|
-
ch,
|
|
23079
|
-
|
|
23080
|
-
|
|
23081
|
-
|
|
23082
|
-
);
|
|
23660
|
+
const supportItem = getSupportItemForChannel(support, ch);
|
|
23661
|
+
const capabilities = computeDeviceCapabilities({
|
|
23662
|
+
channel: ch,
|
|
23663
|
+
...support != null && { support },
|
|
23664
|
+
...abilities != null && { abilities }
|
|
23665
|
+
});
|
|
23083
23666
|
const item = supportItem;
|
|
23084
23667
|
const lightType = item?.lightType;
|
|
23085
23668
|
const ledCtrl = item?.ledCtrl;
|
|
@@ -23095,6 +23678,25 @@ ${xml}`
|
|
|
23095
23678
|
});
|
|
23096
23679
|
capabilities.hasFloodlight = probed;
|
|
23097
23680
|
}
|
|
23681
|
+
let dingDongListIds;
|
|
23682
|
+
let dingDongCfgIds;
|
|
23683
|
+
let wirelessChimeError;
|
|
23684
|
+
if (capabilities.hasWirelessChime) {
|
|
23685
|
+
try {
|
|
23686
|
+
const list = await this.getDingDongList(ch);
|
|
23687
|
+
dingDongListIds = list.map((d) => d.id);
|
|
23688
|
+
const first = list[0];
|
|
23689
|
+
const fromList = first !== void 0 && first.id >= 0;
|
|
23690
|
+
if (!fromList) {
|
|
23691
|
+
const configs = await this.getDingDongCfg(ch);
|
|
23692
|
+
dingDongCfgIds = configs.map((c) => c.id);
|
|
23693
|
+
capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
|
|
23694
|
+
}
|
|
23695
|
+
} catch (e) {
|
|
23696
|
+
capabilities.hasWirelessChime = false;
|
|
23697
|
+
wirelessChimeError = e instanceof Error ? e.message : String(e);
|
|
23698
|
+
}
|
|
23699
|
+
}
|
|
23098
23700
|
const features = this.parseFeaturesFromSupport(support);
|
|
23099
23701
|
const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
|
|
23100
23702
|
const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
|
|
@@ -23131,7 +23733,10 @@ ${xml}`
|
|
|
23131
23733
|
...abilities && {
|
|
23132
23734
|
abilityMergedKeyCount: Object.keys(abilities).length
|
|
23133
23735
|
},
|
|
23134
|
-
...support?.items && { supportItemCount: support.items.length }
|
|
23736
|
+
...support?.items && { supportItemCount: support.items.length },
|
|
23737
|
+
...dingDongListIds !== void 0 && { dingDongListIds },
|
|
23738
|
+
...dingDongCfgIds !== void 0 && { dingDongCfgIds },
|
|
23739
|
+
...wirelessChimeError !== void 0 && { wirelessChimeError }
|
|
23135
23740
|
};
|
|
23136
23741
|
const result = {
|
|
23137
23742
|
capabilities,
|
|
@@ -23158,90 +23763,6 @@ ${xml}`
|
|
|
23158
23763
|
this.deviceCapabilitiesCache.clear();
|
|
23159
23764
|
}
|
|
23160
23765
|
}
|
|
23161
|
-
/**
|
|
23162
|
-
* Pick the best SupportItem for a channel.
|
|
23163
|
-
* Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
|
|
23164
|
-
*/
|
|
23165
|
-
pickBestSupportItem(support, channel) {
|
|
23166
|
-
if (!support?.items?.length) return void 0;
|
|
23167
|
-
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
23168
|
-
if (!candidates.length) return void 0;
|
|
23169
|
-
const score = (item) => {
|
|
23170
|
-
const anyItem = item;
|
|
23171
|
-
let result = 0;
|
|
23172
|
-
if (anyItem.name == null) result += 100;
|
|
23173
|
-
const capabilityKeys = [
|
|
23174
|
-
"ptzType",
|
|
23175
|
-
"ptzControl",
|
|
23176
|
-
"ptzPreset",
|
|
23177
|
-
"ledCtrl",
|
|
23178
|
-
"lightType",
|
|
23179
|
-
"battery",
|
|
23180
|
-
"audioVersion",
|
|
23181
|
-
"motion",
|
|
23182
|
-
"encCtrl",
|
|
23183
|
-
"newIspCfg",
|
|
23184
|
-
"remoteAbility",
|
|
23185
|
-
"aitype",
|
|
23186
|
-
"videoClip",
|
|
23187
|
-
"snap"
|
|
23188
|
-
];
|
|
23189
|
-
for (const k of capabilityKeys) {
|
|
23190
|
-
if (anyItem[k] !== void 0) result += 3;
|
|
23191
|
-
}
|
|
23192
|
-
return result;
|
|
23193
|
-
};
|
|
23194
|
-
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
23195
|
-
}
|
|
23196
|
-
/**
|
|
23197
|
-
* Parse device capabilities from SupportInfo.
|
|
23198
|
-
* Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
|
|
23199
|
-
*/
|
|
23200
|
-
parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
|
|
23201
|
-
const truthy = (v) => {
|
|
23202
|
-
if (typeof v === "number") return v > 0;
|
|
23203
|
-
if (typeof v === "string") {
|
|
23204
|
-
const n = Number(v);
|
|
23205
|
-
return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
|
|
23206
|
-
}
|
|
23207
|
-
return Boolean(v);
|
|
23208
|
-
};
|
|
23209
|
-
const item = supportItem;
|
|
23210
|
-
const ptzMode = support?.ptzMode?.toLowerCase();
|
|
23211
|
-
const ptzType = item ? truthy(item.ptzType) : false;
|
|
23212
|
-
const ptzControl = item ? truthy(item.ptzControl) : false;
|
|
23213
|
-
const hasPtzFromItem = ptzType || ptzControl;
|
|
23214
|
-
const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
|
|
23215
|
-
const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
|
|
23216
|
-
const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
|
|
23217
|
-
const hasPresets = item ? truthy(item.ptzPreset) : false;
|
|
23218
|
-
const hasBattery = item ? truthy(item.battery) : false;
|
|
23219
|
-
const hasSiren = item ? truthy(item.audioVersion) : false;
|
|
23220
|
-
const lightType = item?.lightType;
|
|
23221
|
-
const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
|
|
23222
|
-
const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
|
|
23223
|
-
const isDoorbell = item ? truthy(item.doorbellVersion) : false;
|
|
23224
|
-
const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
|
|
23225
|
-
return {
|
|
23226
|
-
channel,
|
|
23227
|
-
...ptzMode && { ptzMode },
|
|
23228
|
-
hasPan: hasPanTilt,
|
|
23229
|
-
hasTilt: hasPanTilt,
|
|
23230
|
-
hasZoom,
|
|
23231
|
-
hasPresets,
|
|
23232
|
-
hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
|
|
23233
|
-
hasBattery,
|
|
23234
|
-
hasIntercom,
|
|
23235
|
-
hasSiren,
|
|
23236
|
-
hasFloodlight,
|
|
23237
|
-
hasPir,
|
|
23238
|
-
isDoorbell,
|
|
23239
|
-
// Autotracking: explicit flags only (autoPt or smartAI)
|
|
23240
|
-
// Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
|
|
23241
|
-
// on cameras that have PTZ and AI detection but NOT autotracking capability.
|
|
23242
|
-
hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
|
|
23243
|
-
};
|
|
23244
|
-
}
|
|
23245
23766
|
/**
|
|
23246
23767
|
* Parse support features from SupportInfo.
|
|
23247
23768
|
*/
|
|
@@ -26114,6 +26635,216 @@ ${scheduleItems}
|
|
|
26114
26635
|
const channel = 0;
|
|
26115
26636
|
return await this.getSnapshot(channel);
|
|
26116
26637
|
}
|
|
26638
|
+
// --------------------
|
|
26639
|
+
// Chime / DingDong APIs
|
|
26640
|
+
// --------------------
|
|
26641
|
+
/**
|
|
26642
|
+
* Get the list of paired wireless chime devices.
|
|
26643
|
+
* cmd_id: 484 (GetDingDongList)
|
|
26644
|
+
*
|
|
26645
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26646
|
+
* @returns Array of paired chime devices
|
|
26647
|
+
*/
|
|
26648
|
+
async getDingDongList(channel) {
|
|
26649
|
+
const ch = this.normalizeChannel(channel);
|
|
26650
|
+
const xml = await this.sendXml({
|
|
26651
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
|
|
26652
|
+
channel: ch
|
|
26653
|
+
});
|
|
26654
|
+
return parseDingDongListFromXml(xml);
|
|
26655
|
+
}
|
|
26656
|
+
/**
|
|
26657
|
+
* Get parameters (name, volume, LED state) for a specific wireless chime.
|
|
26658
|
+
* cmd_id: 485 (DingDongOpt, option getParam)
|
|
26659
|
+
*
|
|
26660
|
+
* @param chimeId - The chime device ID
|
|
26661
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26662
|
+
* @returns Chime parameters
|
|
26663
|
+
*/
|
|
26664
|
+
async getDingDongParams(chimeId, channel) {
|
|
26665
|
+
const ch = this.normalizeChannel(channel);
|
|
26666
|
+
const payloadXml = buildDingDongGetParamsXml(chimeId);
|
|
26667
|
+
const xml = await this.sendXml({
|
|
26668
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
26669
|
+
channel: ch,
|
|
26670
|
+
payloadXml
|
|
26671
|
+
});
|
|
26672
|
+
return parseDingDongParamsFromXml(xml);
|
|
26673
|
+
}
|
|
26674
|
+
/**
|
|
26675
|
+
* Set parameters (name, volume, LED state) for a specific wireless chime.
|
|
26676
|
+
* cmd_id: 485 (DingDongOpt, option setParam)
|
|
26677
|
+
*
|
|
26678
|
+
* @param chimeId - The chime device ID
|
|
26679
|
+
* @param params - Parameters to set (volLevel, ledState, name)
|
|
26680
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26681
|
+
*/
|
|
26682
|
+
async setDingDongParams(chimeId, params, channel) {
|
|
26683
|
+
const ch = this.normalizeChannel(channel);
|
|
26684
|
+
const payloadXml = buildDingDongSetParamsXml(chimeId, params);
|
|
26685
|
+
await this.sendXml({
|
|
26686
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
26687
|
+
channel: ch,
|
|
26688
|
+
payloadXml
|
|
26689
|
+
});
|
|
26690
|
+
}
|
|
26691
|
+
/**
|
|
26692
|
+
* Trigger a wireless chime to ring with a specific ringtone.
|
|
26693
|
+
* cmd_id: 485 (DingDongOpt, option ringWithMusic)
|
|
26694
|
+
*
|
|
26695
|
+
* @param chimeId - The chime device ID
|
|
26696
|
+
* @param musicId - The ringtone/music ID to play
|
|
26697
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26698
|
+
*/
|
|
26699
|
+
async ringDingDong(chimeId, musicId, channel) {
|
|
26700
|
+
const ch = this.normalizeChannel(channel);
|
|
26701
|
+
const payloadXml = buildDingDongRingXml(chimeId, musicId);
|
|
26702
|
+
await this.sendXml({
|
|
26703
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
26704
|
+
channel: ch,
|
|
26705
|
+
payloadXml
|
|
26706
|
+
});
|
|
26707
|
+
}
|
|
26708
|
+
/**
|
|
26709
|
+
* Get the per-event alarm configuration for paired wireless chimes.
|
|
26710
|
+
* cmd_id: 486 (GetDingDongCfg)
|
|
26711
|
+
*
|
|
26712
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26713
|
+
* @returns Array of chime configurations (one per paired chime)
|
|
26714
|
+
*/
|
|
26715
|
+
async getDingDongCfg(channel) {
|
|
26716
|
+
const ch = this.normalizeChannel(channel);
|
|
26717
|
+
const xml = await this.sendXml({
|
|
26718
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
|
|
26719
|
+
channel: ch
|
|
26720
|
+
});
|
|
26721
|
+
return parseDingDongCfgFromXml(xml);
|
|
26722
|
+
}
|
|
26723
|
+
/**
|
|
26724
|
+
* Set the per-event alarm configuration for a specific wireless chime.
|
|
26725
|
+
* cmd_id: 487 (SetDingDongCfg)
|
|
26726
|
+
*
|
|
26727
|
+
* @param chimeId - The chime ring/device ID
|
|
26728
|
+
* @param eventType - Event type string (e.g. "doorbell", "package", "people")
|
|
26729
|
+
* @param state - 0 = disabled, 1 = enabled
|
|
26730
|
+
* @param musicId - Ringtone ID to use for this event type
|
|
26731
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26732
|
+
*/
|
|
26733
|
+
async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
|
|
26734
|
+
const ch = this.normalizeChannel(channel);
|
|
26735
|
+
const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
|
|
26736
|
+
await this.sendXml({
|
|
26737
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
|
|
26738
|
+
channel: ch,
|
|
26739
|
+
payloadXml
|
|
26740
|
+
});
|
|
26741
|
+
}
|
|
26742
|
+
/** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
|
|
26743
|
+
_hardwiredChimeCache = /* @__PURE__ */ new Map();
|
|
26744
|
+
/**
|
|
26745
|
+
* Get the hardwired (wired-in) chime state.
|
|
26746
|
+
* cmd_id: 483 (GetDingDongCtrl)
|
|
26747
|
+
*
|
|
26748
|
+
* Note: calling this may briefly trigger the physical chime to rattle.
|
|
26749
|
+
*
|
|
26750
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26751
|
+
* @returns Hardwired chime state (type, enabled, time)
|
|
26752
|
+
*/
|
|
26753
|
+
async getHardwiredChime(channel) {
|
|
26754
|
+
const ch = this.normalizeChannel(channel);
|
|
26755
|
+
const payloadXml = buildGetDingDongCtrlXml();
|
|
26756
|
+
const xml = await this.sendXml({
|
|
26757
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
26758
|
+
channel: ch,
|
|
26759
|
+
payloadXml
|
|
26760
|
+
});
|
|
26761
|
+
const state = parseHardwiredChimeFromXml(xml);
|
|
26762
|
+
this._hardwiredChimeCache.set(ch, state);
|
|
26763
|
+
return state;
|
|
26764
|
+
}
|
|
26765
|
+
/**
|
|
26766
|
+
* Set the hardwired (wired-in) chime state.
|
|
26767
|
+
* cmd_id: 483 (SetDingDongCtrl)
|
|
26768
|
+
*
|
|
26769
|
+
* Uses the cached state from a previous getHardwiredChime call to fill in
|
|
26770
|
+
* missing type/time fields, avoiding a double round-trip on every set.
|
|
26771
|
+
* Falls back to fetching if no cache is available.
|
|
26772
|
+
*
|
|
26773
|
+
* @param params - Chime configuration (type, enabled, time)
|
|
26774
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26775
|
+
*/
|
|
26776
|
+
async setHardwiredChime(params, channel) {
|
|
26777
|
+
const ch = this.normalizeChannel(channel);
|
|
26778
|
+
let current = this._hardwiredChimeCache.get(ch);
|
|
26779
|
+
if (!current) {
|
|
26780
|
+
current = await this.getHardwiredChime(ch);
|
|
26781
|
+
}
|
|
26782
|
+
const chimeType = params.type ?? current.type;
|
|
26783
|
+
const enabled = params.enabled ? 1 : 0;
|
|
26784
|
+
const time = params.time ?? current.time;
|
|
26785
|
+
const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
|
|
26786
|
+
const xml = await this.sendXml({
|
|
26787
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
26788
|
+
channel: ch,
|
|
26789
|
+
payloadXml
|
|
26790
|
+
});
|
|
26791
|
+
const newState = parseHardwiredChimeFromXml(xml);
|
|
26792
|
+
this._hardwiredChimeCache.set(ch, newState);
|
|
26793
|
+
return newState;
|
|
26794
|
+
}
|
|
26795
|
+
/**
|
|
26796
|
+
* Play an audio file on the doorbell / chime device.
|
|
26797
|
+
* cmd_id: 349 (QuickReplyPlay)
|
|
26798
|
+
*
|
|
26799
|
+
* @param fileId - The audio file ID to play
|
|
26800
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26801
|
+
*/
|
|
26802
|
+
async quickReplyPlay(fileId, channel) {
|
|
26803
|
+
const ch = this.normalizeChannel(channel);
|
|
26804
|
+
const payloadXml = buildQuickReplyPlayXml(ch, fileId);
|
|
26805
|
+
await this.sendXml({
|
|
26806
|
+
cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
26807
|
+
channel: ch,
|
|
26808
|
+
payloadXml
|
|
26809
|
+
});
|
|
26810
|
+
}
|
|
26811
|
+
/**
|
|
26812
|
+
* Get the silent mode state of a paired wireless chime.
|
|
26813
|
+
* cmd_id: 609 (GetDingDongSilent)
|
|
26814
|
+
*
|
|
26815
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
26816
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26817
|
+
* @returns Wireless chime silent state (time=0 means active/not silenced)
|
|
26818
|
+
*/
|
|
26819
|
+
async getDingDongSilent(chimeId, channel) {
|
|
26820
|
+
const ch = this.normalizeChannel(channel);
|
|
26821
|
+
const payloadXml = buildGetDingDongSilentXml(chimeId);
|
|
26822
|
+
const xml = await this.sendXml({
|
|
26823
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
26824
|
+
channel: ch,
|
|
26825
|
+
payloadXml
|
|
26826
|
+
});
|
|
26827
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
26828
|
+
}
|
|
26829
|
+
/**
|
|
26830
|
+
* Set the silent mode of a paired wireless chime.
|
|
26831
|
+
* cmd_id: 610 (SetDingDongSilent)
|
|
26832
|
+
*
|
|
26833
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
26834
|
+
* @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
|
|
26835
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26836
|
+
* @returns Updated wireless chime silent state
|
|
26837
|
+
*/
|
|
26838
|
+
async setDingDongSilent(chimeId, time, channel) {
|
|
26839
|
+
const ch = this.normalizeChannel(channel);
|
|
26840
|
+
const payloadXml = buildSetDingDongSilentXml(chimeId, time);
|
|
26841
|
+
const xml = await this.sendXml({
|
|
26842
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
26843
|
+
channel: ch,
|
|
26844
|
+
payloadXml
|
|
26845
|
+
});
|
|
26846
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
26847
|
+
}
|
|
26117
26848
|
};
|
|
26118
26849
|
|
|
26119
26850
|
// src/reolink/autodetect.ts
|