@apocaliss92/nodelink-js 0.2.1 → 0.2.3
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 +1 -1
- package/dist/{DiagnosticsTools-NUMCYEKQ.js → DiagnosticsTools-FNLGCOVA.js} +2 -2
- package/dist/{chunk-YPU7RAEY.js → chunk-NLTB7GTA.js} +17 -1
- package/dist/{chunk-YPU7RAEY.js.map → chunk-NLTB7GTA.js.map} +1 -1
- package/dist/{chunk-PCPEXOWB.js → chunk-RWYEGEWG.js} +832 -224
- package/dist/chunk-RWYEGEWG.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +829 -221
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +853 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +239 -12
- package/dist/index.d.ts +253 -11
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PCPEXOWB.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
|
}
|
|
@@ -7641,6 +7651,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7641
7651
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
7642
7652
|
nativeFanout = null;
|
|
7643
7653
|
noClientAutoStopTimer;
|
|
7654
|
+
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
7655
|
+
// When a new client connects while the stream is already running it does not need
|
|
7656
|
+
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
7657
|
+
// from the last IDR in the prebuffer immediately.
|
|
7658
|
+
PREBUFFER_MAX_MS = 3e3;
|
|
7659
|
+
prebuffer = [];
|
|
7644
7660
|
static isAdtsAacFrame(b) {
|
|
7645
7661
|
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
7646
7662
|
}
|
|
@@ -7675,6 +7691,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7675
7691
|
);
|
|
7676
7692
|
return { sampleRate, channels, configHex };
|
|
7677
7693
|
}
|
|
7694
|
+
/** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
|
|
7695
|
+
isRawFrameKeyframe(frame) {
|
|
7696
|
+
try {
|
|
7697
|
+
if (frame.videoType === "H264") {
|
|
7698
|
+
const nals = _BaichuanRtspServer.splitAnnexBNals(
|
|
7699
|
+
convertToAnnexB(frame.data)
|
|
7700
|
+
);
|
|
7701
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
7702
|
+
}
|
|
7703
|
+
if (frame.videoType === "H265") {
|
|
7704
|
+
const nals = splitAnnexBToNalPayloads2(convertToAnnexB2(frame.data));
|
|
7705
|
+
return nals.some(
|
|
7706
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
7707
|
+
);
|
|
7708
|
+
}
|
|
7709
|
+
} catch {
|
|
7710
|
+
}
|
|
7711
|
+
return false;
|
|
7712
|
+
}
|
|
7678
7713
|
static parseInterleavedChannels(transportHeader) {
|
|
7679
7714
|
const m = transportHeader.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
|
|
7680
7715
|
if (!m) return null;
|
|
@@ -7867,7 +7902,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7867
7902
|
this.logger.warn(
|
|
7868
7903
|
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
7869
7904
|
);
|
|
7870
|
-
this.streamMetadata = { frameRate: 25
|
|
7905
|
+
this.streamMetadata = { frameRate: 25 };
|
|
7871
7906
|
this.setFlowVideoType("H264", "metadata unavailable");
|
|
7872
7907
|
}
|
|
7873
7908
|
this.clientConnectionServer = net.createServer((socket) => {
|
|
@@ -7899,7 +7934,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7899
7934
|
*/
|
|
7900
7935
|
handleRtspConnection(socket) {
|
|
7901
7936
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
7902
|
-
|
|
7937
|
+
const connectTime = Date.now();
|
|
7938
|
+
this.logger.info(
|
|
7939
|
+
`[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
|
|
7940
|
+
);
|
|
7903
7941
|
let sessionId = "";
|
|
7904
7942
|
let buffer = Buffer.alloc(0);
|
|
7905
7943
|
let clientFfmpeg;
|
|
@@ -7907,6 +7945,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
7907
7945
|
let clientUdpSocket = null;
|
|
7908
7946
|
let clientUdpSocketAudio = null;
|
|
7909
7947
|
const cleanup = () => {
|
|
7948
|
+
const sessionDurationMs = Date.now() - connectTime;
|
|
7949
|
+
const res = this.clientResources.get(clientId);
|
|
7950
|
+
const framesSent = res?.framesSent ?? 0;
|
|
7951
|
+
this.logger.info(
|
|
7952
|
+
`[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
|
|
7953
|
+
);
|
|
7910
7954
|
this.removeClient(clientId);
|
|
7911
7955
|
this.authNonces.delete(clientId);
|
|
7912
7956
|
const resources = this.clientResources.get(clientId);
|
|
@@ -8048,7 +8092,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8048
8092
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
8049
8093
|
});
|
|
8050
8094
|
} else if (method === "DESCRIBE") {
|
|
8051
|
-
if (!this.
|
|
8095
|
+
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
8052
8096
|
try {
|
|
8053
8097
|
if (!this.nativeStreamActive) {
|
|
8054
8098
|
await this.startNativeStream();
|
|
@@ -8060,7 +8104,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8060
8104
|
}
|
|
8061
8105
|
const { hasParamSets } = this.flow.getFmtp();
|
|
8062
8106
|
if (!hasParamSets) {
|
|
8063
|
-
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 :
|
|
8107
|
+
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 3e3;
|
|
8108
|
+
const primingStart = Date.now();
|
|
8109
|
+
this.logger.info(
|
|
8110
|
+
`[rebroadcast] DESCRIBE priming: waiting up to ${primingMs}ms for SPS/PPS client=${clientId} path=${this.path}`
|
|
8111
|
+
);
|
|
8064
8112
|
try {
|
|
8065
8113
|
await Promise.race([
|
|
8066
8114
|
this.firstFramePromise || Promise.resolve(),
|
|
@@ -8068,6 +8116,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8068
8116
|
]);
|
|
8069
8117
|
} catch {
|
|
8070
8118
|
}
|
|
8119
|
+
const primingElapsed = Date.now() - primingStart;
|
|
8120
|
+
const { hasParamSets: hasParamSetsAfter } = this.flow.getFmtp();
|
|
8121
|
+
if (hasParamSetsAfter) {
|
|
8122
|
+
this.logger.info(
|
|
8123
|
+
`[rebroadcast] DESCRIBE priming: SPS/PPS received after ${primingElapsed}ms client=${clientId} path=${this.path}`
|
|
8124
|
+
);
|
|
8125
|
+
} else {
|
|
8126
|
+
this.logger.warn(
|
|
8127
|
+
`[rebroadcast] DESCRIBE priming: timed out after ${primingElapsed}ms without SPS/PPS \u2014 SDP will lack sprop-parameter-sets, downstream decoder may hang client=${clientId} path=${this.path}`
|
|
8128
|
+
);
|
|
8129
|
+
}
|
|
8071
8130
|
}
|
|
8072
8131
|
}
|
|
8073
8132
|
{
|
|
@@ -8076,11 +8135,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8076
8135
|
this.logger.info(
|
|
8077
8136
|
`[BaichuanRtspServer] DESCRIBE SDP for ${clientId} path=${this.path} codec=${this.flow.sdpCodec} hasParamSets=${hasParamSets} fmtp=${fmtpPreview}`
|
|
8078
8137
|
);
|
|
8079
|
-
if (!hasParamSets) {
|
|
8080
|
-
this.rtspDebugLog(
|
|
8081
|
-
`DESCRIBE responding without parameter sets yet (client=${clientId}, path=${this.path}, flow=${this.flow.key})`
|
|
8082
|
-
);
|
|
8083
|
-
}
|
|
8084
8138
|
}
|
|
8085
8139
|
const sdp = this.generateSdp();
|
|
8086
8140
|
sendResponse(
|
|
@@ -8130,7 +8184,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8130
8184
|
seenFirstVideoKeyframe: false,
|
|
8131
8185
|
setupTrack0: false,
|
|
8132
8186
|
setupTrack1: false,
|
|
8133
|
-
isPlaying: false
|
|
8187
|
+
isPlaying: false,
|
|
8188
|
+
connectTime
|
|
8134
8189
|
});
|
|
8135
8190
|
} else {
|
|
8136
8191
|
existing.rtspSocket = socket;
|
|
@@ -8177,8 +8232,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8177
8232
|
if (resources) {
|
|
8178
8233
|
if (isTrack1) resources.setupTrack1 = true;
|
|
8179
8234
|
else resources.setupTrack0 = true;
|
|
8180
|
-
|
|
8181
|
-
|
|
8235
|
+
const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
|
|
8236
|
+
const track = isTrack1 ? "track1(audio)" : "track0(video)";
|
|
8237
|
+
this.logger.info(
|
|
8238
|
+
`[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
|
|
8182
8239
|
);
|
|
8183
8240
|
}
|
|
8184
8241
|
}
|
|
@@ -8203,8 +8260,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8203
8260
|
const resources = this.clientResources.get(clientId);
|
|
8204
8261
|
if (resources) {
|
|
8205
8262
|
resources.isPlaying = true;
|
|
8206
|
-
|
|
8207
|
-
|
|
8263
|
+
const hasAudio = !!resources.setupTrack1;
|
|
8264
|
+
this.logger.info(
|
|
8265
|
+
`[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
|
|
8208
8266
|
);
|
|
8209
8267
|
}
|
|
8210
8268
|
}
|
|
@@ -8213,6 +8271,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8213
8271
|
Range: "npt=0.000-"
|
|
8214
8272
|
});
|
|
8215
8273
|
} else if (method === "TEARDOWN") {
|
|
8274
|
+
this.logger.info(
|
|
8275
|
+
`[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
|
|
8276
|
+
);
|
|
8216
8277
|
cleanup();
|
|
8217
8278
|
sendResponse(200, "OK", {
|
|
8218
8279
|
Session: sessionId
|
|
@@ -8278,10 +8339,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8278
8339
|
sdp += `a=control:track1\r
|
|
8279
8340
|
`;
|
|
8280
8341
|
}
|
|
8281
|
-
sdp += `a=setup:passive\r
|
|
8282
|
-
`;
|
|
8283
|
-
sdp += `a=connection:new\r
|
|
8284
|
-
`;
|
|
8285
8342
|
return sdp;
|
|
8286
8343
|
}
|
|
8287
8344
|
/**
|
|
@@ -8307,7 +8364,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8307
8364
|
this.logger.warn(
|
|
8308
8365
|
`[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
|
|
8309
8366
|
);
|
|
8310
|
-
streamMetadata = { frameRate: 25
|
|
8367
|
+
streamMetadata = { frameRate: 25 };
|
|
8311
8368
|
}
|
|
8312
8369
|
}
|
|
8313
8370
|
const ffmpegFormat = this.flow.ffmpegFormat;
|
|
@@ -8352,6 +8409,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8352
8409
|
return false;
|
|
8353
8410
|
if (channel === audioRtpChannel && !resources2?.setupTrack1)
|
|
8354
8411
|
return false;
|
|
8412
|
+
const buffered = rtspSocket.writableLength;
|
|
8413
|
+
if (buffered > 10 * 1024 * 1024) {
|
|
8414
|
+
this.logger.warn(
|
|
8415
|
+
`[rebroadcast] backpressure: ${Math.round(buffered / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
|
|
8416
|
+
);
|
|
8417
|
+
rtspSocket.destroy();
|
|
8418
|
+
return false;
|
|
8419
|
+
}
|
|
8355
8420
|
try {
|
|
8356
8421
|
return rtspSocket.write(frameRtpOverTcp(channel, msg));
|
|
8357
8422
|
} catch (error) {
|
|
@@ -8781,6 +8846,24 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8781
8846
|
let frameCount = 0;
|
|
8782
8847
|
let lastFrameTime = Date.now();
|
|
8783
8848
|
const targetFrameInterval = streamMetadata && streamMetadata.frameRate > 0 ? 1e3 / streamMetadata.frameRate : 40;
|
|
8849
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
8850
|
+
let lastIdrIdx = -1;
|
|
8851
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
8852
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
8853
|
+
lastIdrIdx = i;
|
|
8854
|
+
break;
|
|
8855
|
+
}
|
|
8856
|
+
}
|
|
8857
|
+
const prebufferFrames = lastIdrIdx >= 0 ? prebufferSnap.slice(lastIdrIdx) : [];
|
|
8858
|
+
if (prebufferFrames.length > 0) {
|
|
8859
|
+
this.logger.info(
|
|
8860
|
+
`[rebroadcast] prebuffer replay client=${clientId} frames=${prebufferFrames.length} starting from IDR`
|
|
8861
|
+
);
|
|
8862
|
+
}
|
|
8863
|
+
const combined = async function* () {
|
|
8864
|
+
for (const entry of prebufferFrames) yield entry.frame;
|
|
8865
|
+
for await (const f of clientGenerator) yield f;
|
|
8866
|
+
};
|
|
8784
8867
|
const feedFrames = async () => {
|
|
8785
8868
|
try {
|
|
8786
8869
|
this.rtspDebugLog(
|
|
@@ -8792,7 +8875,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8792
8875
|
let firstVideoFrameSeenLogged = false;
|
|
8793
8876
|
let h265WaitParamSetsLogged = false;
|
|
8794
8877
|
let h265WaitIrapLogged = false;
|
|
8795
|
-
for await (const frame of
|
|
8878
|
+
for await (const frame of combined()) {
|
|
8796
8879
|
if (!this.connectedClients.has(clientId)) {
|
|
8797
8880
|
this.rtspDebugLog(
|
|
8798
8881
|
`Client ${clientId} disconnected, stopping frame feed`
|
|
@@ -8895,15 +8978,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8895
8978
|
`Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
|
|
8896
8979
|
);
|
|
8897
8980
|
}
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8981
|
+
if (!useDirectRtp) {
|
|
8982
|
+
const now = Date.now();
|
|
8983
|
+
const timeSinceLastFrame = now - lastFrameTime;
|
|
8984
|
+
const waitTime = targetFrameInterval - timeSinceLastFrame;
|
|
8985
|
+
if (waitTime > 0) {
|
|
8986
|
+
await new Promise(
|
|
8987
|
+
(resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
|
|
8988
|
+
);
|
|
8989
|
+
}
|
|
8990
|
+
lastFrameTime = Date.now();
|
|
8905
8991
|
}
|
|
8906
|
-
lastFrameTime = Date.now();
|
|
8907
8992
|
if (useDirectRtp) {
|
|
8908
8993
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
8909
8994
|
const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
|
|
@@ -8976,6 +9061,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8976
9061
|
}
|
|
8977
9062
|
if (!firstVideoWriteLogged) {
|
|
8978
9063
|
firstVideoWriteLogged = true;
|
|
9064
|
+
const clientConnectTime = resources?.connectTime ?? Date.now();
|
|
9065
|
+
const ttffMs = Date.now() - clientConnectTime;
|
|
9066
|
+
this.logger.info(
|
|
9067
|
+
`[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
|
|
9068
|
+
);
|
|
8979
9069
|
if (rtspDebug) {
|
|
8980
9070
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
8981
9071
|
rtspDebugLog(
|
|
@@ -8983,6 +9073,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8983
9073
|
);
|
|
8984
9074
|
}
|
|
8985
9075
|
}
|
|
9076
|
+
if (resources) {
|
|
9077
|
+
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
9078
|
+
}
|
|
8986
9079
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
8987
9080
|
} else {
|
|
8988
9081
|
try {
|
|
@@ -9067,8 +9160,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9067
9160
|
this.firstAudioPromise = new Promise((resolve) => {
|
|
9068
9161
|
this.firstAudioResolve = resolve;
|
|
9069
9162
|
});
|
|
9070
|
-
this.
|
|
9071
|
-
`
|
|
9163
|
+
this.logger.info(
|
|
9164
|
+
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
9072
9165
|
);
|
|
9073
9166
|
await this.flow.startKeepAlive(this.api);
|
|
9074
9167
|
this.nativeFanout = new NativeStreamFanout({
|
|
@@ -9106,11 +9199,41 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9106
9199
|
if (hasParamSets) {
|
|
9107
9200
|
this.markFirstFrameReceived();
|
|
9108
9201
|
}
|
|
9202
|
+
const isKeyframe = this.isRawFrameKeyframe(frame);
|
|
9203
|
+
this.prebuffer.push({
|
|
9204
|
+
frame: { ...frame, data: Buffer.from(frame.data) },
|
|
9205
|
+
time: Date.now(),
|
|
9206
|
+
isKeyframe
|
|
9207
|
+
});
|
|
9208
|
+
const cutoff = Date.now() - this.PREBUFFER_MAX_MS;
|
|
9209
|
+
let trimIdx = 0;
|
|
9210
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
9211
|
+
trimIdx++;
|
|
9212
|
+
}
|
|
9213
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
9109
9214
|
},
|
|
9110
9215
|
onError: (error) => {
|
|
9111
9216
|
this.logger.warn(
|
|
9112
9217
|
`[BaichuanRtspServer] Shared native stream error: ${error}`
|
|
9113
9218
|
);
|
|
9219
|
+
},
|
|
9220
|
+
onEnd: () => {
|
|
9221
|
+
if (!this.nativeStreamActive) return;
|
|
9222
|
+
this.nativeStreamActive = false;
|
|
9223
|
+
this.firstFrameReceived = false;
|
|
9224
|
+
this.firstFramePromise = null;
|
|
9225
|
+
this.firstFrameResolve = null;
|
|
9226
|
+
this.nativeFanout = null;
|
|
9227
|
+
this.prebuffer = [];
|
|
9228
|
+
this.logger.info(
|
|
9229
|
+
`[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
9230
|
+
);
|
|
9231
|
+
if (this.connectedClients.size > 0) {
|
|
9232
|
+
this.logger.info(
|
|
9233
|
+
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
9234
|
+
);
|
|
9235
|
+
setImmediate(() => void this.startNativeStream());
|
|
9236
|
+
}
|
|
9114
9237
|
}
|
|
9115
9238
|
});
|
|
9116
9239
|
this.nativeFanout.start();
|
|
@@ -9149,7 +9272,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9149
9272
|
if (!this.nativeStreamActive) {
|
|
9150
9273
|
return;
|
|
9151
9274
|
}
|
|
9152
|
-
this.
|
|
9275
|
+
this.logger.info(
|
|
9276
|
+
`[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
9277
|
+
);
|
|
9153
9278
|
this.flow.stopKeepAlive();
|
|
9154
9279
|
this.clearNoClientAutoStopTimer();
|
|
9155
9280
|
this.nativeStreamActive = false;
|
|
@@ -9168,6 +9293,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9168
9293
|
this.nativeFanout = null;
|
|
9169
9294
|
await fanout.stop();
|
|
9170
9295
|
}
|
|
9296
|
+
this.prebuffer = [];
|
|
9171
9297
|
if (this.tempStreamGenerator) {
|
|
9172
9298
|
try {
|
|
9173
9299
|
await this.tempStreamGenerator.return(void 0);
|
|
@@ -9183,9 +9309,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9183
9309
|
if (this.connectedClients.has(clientId)) {
|
|
9184
9310
|
this.connectedClients.delete(clientId);
|
|
9185
9311
|
this.emit("clientDisconnected", clientId);
|
|
9186
|
-
this.logger.info(
|
|
9187
|
-
`[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
|
|
9188
|
-
);
|
|
9189
9312
|
if (this.connectedClients.size === 0) {
|
|
9190
9313
|
void this.stopNativeStream();
|
|
9191
9314
|
}
|
|
@@ -14392,10 +14515,12 @@ function parseSupportXml(xml) {
|
|
|
14392
14515
|
}
|
|
14393
14516
|
function getSupportItemForChannel(support, channel) {
|
|
14394
14517
|
if (!support?.items?.length) return void 0;
|
|
14395
|
-
const
|
|
14518
|
+
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
14519
|
+
if (!candidates.length) return void 0;
|
|
14520
|
+
const score = (item) => {
|
|
14396
14521
|
const anyItem = item;
|
|
14397
|
-
let
|
|
14398
|
-
if (anyItem.name == null)
|
|
14522
|
+
let result = 0;
|
|
14523
|
+
if (anyItem.name == null) result += 100;
|
|
14399
14524
|
const capabilityKeys = [
|
|
14400
14525
|
"ptzType",
|
|
14401
14526
|
"ptzControl",
|
|
@@ -14407,20 +14532,17 @@ function getSupportItemForChannel(support, channel) {
|
|
|
14407
14532
|
"motion",
|
|
14408
14533
|
"encCtrl",
|
|
14409
14534
|
"newIspCfg",
|
|
14410
|
-
"remoteAbility"
|
|
14535
|
+
"remoteAbility",
|
|
14536
|
+
"aitype",
|
|
14537
|
+
"videoClip",
|
|
14538
|
+
"snap"
|
|
14411
14539
|
];
|
|
14412
14540
|
for (const k of capabilityKeys) {
|
|
14413
|
-
if (anyItem[k] !== void 0)
|
|
14541
|
+
if (anyItem[k] !== void 0) result += 3;
|
|
14414
14542
|
}
|
|
14415
|
-
|
|
14416
|
-
return score;
|
|
14417
|
-
};
|
|
14418
|
-
const pickBest = (chnId) => {
|
|
14419
|
-
const candidates = support.items.filter((i) => i.chnID === chnId);
|
|
14420
|
-
if (!candidates.length) return void 0;
|
|
14421
|
-
return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
|
|
14543
|
+
return result;
|
|
14422
14544
|
};
|
|
14423
|
-
return
|
|
14545
|
+
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
14424
14546
|
}
|
|
14425
14547
|
function computeDeviceCapabilities(params) {
|
|
14426
14548
|
const { channel } = params;
|
|
@@ -14452,6 +14574,7 @@ function computeDeviceCapabilities(params) {
|
|
|
14452
14574
|
flat,
|
|
14453
14575
|
/white\s*led|whiteLed|flood\s*light|floodlight/i
|
|
14454
14576
|
);
|
|
14577
|
+
const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
|
|
14455
14578
|
const hasSirenFromAbilities = abilitiesHasAny(
|
|
14456
14579
|
flat,
|
|
14457
14580
|
/audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
|
|
@@ -14464,6 +14587,9 @@ function computeDeviceCapabilities(params) {
|
|
|
14464
14587
|
const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
|
|
14465
14588
|
const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
|
|
14466
14589
|
const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
|
|
14590
|
+
const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
|
|
14591
|
+
const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
|
|
14592
|
+
const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
|
|
14467
14593
|
const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
14468
14594
|
const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
14469
14595
|
const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
|
|
@@ -14479,14 +14605,15 @@ function computeDeviceCapabilities(params) {
|
|
|
14479
14605
|
hasZoom: finalHasZoom,
|
|
14480
14606
|
hasPresets: finalHasPresets,
|
|
14481
14607
|
hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
|
|
14482
|
-
hasBattery
|
|
14608
|
+
hasBattery,
|
|
14483
14609
|
hasIntercom: hasIntercomFromSupport,
|
|
14484
|
-
hasSiren: hasSirenFromAbilities,
|
|
14610
|
+
hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
|
|
14485
14611
|
// lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
|
|
14486
14612
|
hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
|
|
14487
14613
|
hasPir: hasPirFromAbilities || hasPirFromSupport,
|
|
14488
|
-
isDoorbell
|
|
14489
|
-
hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
|
|
14614
|
+
isDoorbell,
|
|
14615
|
+
hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
|
|
14616
|
+
hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
|
|
14490
14617
|
};
|
|
14491
14618
|
if (ptzMode !== void 0) result.ptzMode = ptzMode;
|
|
14492
14619
|
return result;
|
|
@@ -16151,6 +16278,162 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
|
|
|
16151
16278
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
16152
16279
|
init_recordingFileName();
|
|
16153
16280
|
|
|
16281
|
+
// src/reolink/baichuan/utils/chime.ts
|
|
16282
|
+
init_xml();
|
|
16283
|
+
var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16284
|
+
<body>
|
|
16285
|
+
<dingdongDeviceOpt version="1.1">
|
|
16286
|
+
<id>${chimeId}</id>
|
|
16287
|
+
<opt>getParam</opt>
|
|
16288
|
+
</dingdongDeviceOpt>
|
|
16289
|
+
</body>`;
|
|
16290
|
+
var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16291
|
+
<body>
|
|
16292
|
+
<dingdongDeviceOpt version="1.1">
|
|
16293
|
+
<opt>setParam</opt>
|
|
16294
|
+
<id>${chimeId}</id>
|
|
16295
|
+
${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
|
|
16296
|
+
${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
|
|
16297
|
+
${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
|
|
16298
|
+
</dingdongDeviceOpt>
|
|
16299
|
+
</body>`;
|
|
16300
|
+
var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16301
|
+
<body>
|
|
16302
|
+
<dingdongDeviceOpt version="1.1">
|
|
16303
|
+
<id>${chimeId}</id>
|
|
16304
|
+
<opt>ringWithMusic</opt>
|
|
16305
|
+
<musicId>${musicId}</musicId>
|
|
16306
|
+
</dingdongDeviceOpt>
|
|
16307
|
+
</body>`;
|
|
16308
|
+
var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16309
|
+
<body>
|
|
16310
|
+
<dingdongCfg version="1.1">
|
|
16311
|
+
<deviceCfg>
|
|
16312
|
+
<id>${chimeId}</id>
|
|
16313
|
+
<alarminCfg>
|
|
16314
|
+
<valid>${state}</valid>
|
|
16315
|
+
<musicId>${musicId}</musicId>
|
|
16316
|
+
<type>${eventType}</type>
|
|
16317
|
+
</alarminCfg>
|
|
16318
|
+
</deviceCfg>
|
|
16319
|
+
</dingdongCfg>
|
|
16320
|
+
</body>`;
|
|
16321
|
+
var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16322
|
+
<body>
|
|
16323
|
+
<dingdongCtrl version="1.1">
|
|
16324
|
+
<opt>machineStateGet</opt>
|
|
16325
|
+
</dingdongCtrl>
|
|
16326
|
+
</body>`;
|
|
16327
|
+
var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16328
|
+
<body>
|
|
16329
|
+
<dingdongCtrl version="1.1">
|
|
16330
|
+
<opt>machineStateSet</opt>
|
|
16331
|
+
<type>${chimeType}</type>
|
|
16332
|
+
<bopen>${enabled}</bopen>
|
|
16333
|
+
<bsave>1</bsave>
|
|
16334
|
+
<time>${time}</time>
|
|
16335
|
+
</dingdongCtrl>
|
|
16336
|
+
</body>`;
|
|
16337
|
+
var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16338
|
+
<body>
|
|
16339
|
+
<audioFileInfo version="1.1">
|
|
16340
|
+
<channelId>${channel}</channelId>
|
|
16341
|
+
<id>${fileId}</id>
|
|
16342
|
+
<timeout>0</timeout>
|
|
16343
|
+
</audioFileInfo>
|
|
16344
|
+
</body>`;
|
|
16345
|
+
var parseDingDongListFromXml = (xml) => {
|
|
16346
|
+
const devices = [];
|
|
16347
|
+
const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
|
|
16348
|
+
for (const block of blocks) {
|
|
16349
|
+
const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
|
|
16350
|
+
const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
|
|
16351
|
+
const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
|
|
16352
|
+
if (idText === void 0) continue;
|
|
16353
|
+
const id = Number(idText);
|
|
16354
|
+
if (!Number.isFinite(id)) continue;
|
|
16355
|
+
devices.push({
|
|
16356
|
+
id,
|
|
16357
|
+
name,
|
|
16358
|
+
netState: netStateText !== void 0 ? Number(netStateText) : 0
|
|
16359
|
+
});
|
|
16360
|
+
}
|
|
16361
|
+
return devices;
|
|
16362
|
+
};
|
|
16363
|
+
var parseDingDongParamsFromXml = (xml) => {
|
|
16364
|
+
const name = getXmlText(xml, "name");
|
|
16365
|
+
const volLevelText = getXmlText(xml, "volLevel");
|
|
16366
|
+
const ledStateText = getXmlText(xml, "ledState");
|
|
16367
|
+
const result = {};
|
|
16368
|
+
if (name !== void 0) result.name = name;
|
|
16369
|
+
if (volLevelText !== void 0) {
|
|
16370
|
+
const n = Number(volLevelText);
|
|
16371
|
+
if (Number.isFinite(n)) result.volLevel = n;
|
|
16372
|
+
}
|
|
16373
|
+
if (ledStateText !== void 0) {
|
|
16374
|
+
const n = Number(ledStateText);
|
|
16375
|
+
if (Number.isFinite(n)) result.ledState = n;
|
|
16376
|
+
}
|
|
16377
|
+
return result;
|
|
16378
|
+
};
|
|
16379
|
+
var parseDingDongCfgFromXml = (xml) => {
|
|
16380
|
+
const configs = [];
|
|
16381
|
+
const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
|
|
16382
|
+
for (const deviceBlock of deviceBlocks) {
|
|
16383
|
+
const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
|
|
16384
|
+
if (idText === void 0) continue;
|
|
16385
|
+
const id = Number(idText);
|
|
16386
|
+
if (!Number.isFinite(id)) continue;
|
|
16387
|
+
const typeMap = {};
|
|
16388
|
+
const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
|
|
16389
|
+
for (const alarmBlock of alarmBlocks) {
|
|
16390
|
+
const type = getXmlText(alarmBlock, "type");
|
|
16391
|
+
if (!type) continue;
|
|
16392
|
+
const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
|
|
16393
|
+
const musicIdText = getXmlText(alarmBlock, "musicId");
|
|
16394
|
+
typeMap[type] = {
|
|
16395
|
+
valid: validText !== void 0 ? Number(validText) : 0,
|
|
16396
|
+
musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
|
|
16397
|
+
};
|
|
16398
|
+
}
|
|
16399
|
+
configs.push({ id, type: typeMap });
|
|
16400
|
+
}
|
|
16401
|
+
return configs;
|
|
16402
|
+
};
|
|
16403
|
+
var parseHardwiredChimeFromXml = (xml) => {
|
|
16404
|
+
const type = getXmlText(xml, "type") ?? "";
|
|
16405
|
+
const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
|
|
16406
|
+
const timeText = getXmlText(xml, "time");
|
|
16407
|
+
return {
|
|
16408
|
+
type,
|
|
16409
|
+
enabled: bopenText === "1",
|
|
16410
|
+
time: timeText !== void 0 ? Number(timeText) : 0
|
|
16411
|
+
};
|
|
16412
|
+
};
|
|
16413
|
+
var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16414
|
+
<body>
|
|
16415
|
+
<dingdongSilentMode version="1.1">
|
|
16416
|
+
<id>${chimeId}</id>
|
|
16417
|
+
</dingdongSilentMode>
|
|
16418
|
+
</body>`;
|
|
16419
|
+
var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16420
|
+
<body>
|
|
16421
|
+
<dingdongSilentMode version="1.1">
|
|
16422
|
+
<id>${chimeId}</id>
|
|
16423
|
+
<time>${time}</time>
|
|
16424
|
+
<type>63</type>
|
|
16425
|
+
</dingdongSilentMode>
|
|
16426
|
+
</body>`;
|
|
16427
|
+
var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
|
|
16428
|
+
const timeText = getXmlText(xml, "time");
|
|
16429
|
+
const time = timeText !== void 0 ? Number(timeText) : 0;
|
|
16430
|
+
return {
|
|
16431
|
+
id: chimeId,
|
|
16432
|
+
time,
|
|
16433
|
+
active: time === 0
|
|
16434
|
+
};
|
|
16435
|
+
};
|
|
16436
|
+
|
|
16154
16437
|
// src/reolink/baichuan/utils/eventsGetEvents.ts
|
|
16155
16438
|
init_xml();
|
|
16156
16439
|
var parseAiTypeToken = (aiTypeRaw) => {
|
|
@@ -16463,6 +16746,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
16463
16746
|
host;
|
|
16464
16747
|
username;
|
|
16465
16748
|
password;
|
|
16749
|
+
/**
|
|
16750
|
+
* Set to `true` after `close()` is called.
|
|
16751
|
+
* Once closed, the API instance should not be reused.
|
|
16752
|
+
*/
|
|
16753
|
+
_closed = false;
|
|
16466
16754
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16467
16755
|
// SOCKET POOL - Tag-based socket management
|
|
16468
16756
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -16492,10 +16780,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
16492
16780
|
get client() {
|
|
16493
16781
|
const entry = this.socketPool.get("general");
|
|
16494
16782
|
if (!entry) {
|
|
16783
|
+
if (this._closed) {
|
|
16784
|
+
throw new Error(
|
|
16785
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
16786
|
+
);
|
|
16787
|
+
}
|
|
16495
16788
|
throw new Error("[ReolinkBaichuanApi] General socket not initialized");
|
|
16496
16789
|
}
|
|
16497
16790
|
return entry.client;
|
|
16498
16791
|
}
|
|
16792
|
+
/**
|
|
16793
|
+
* `true` after `close()` has been called. A closed API should not be reused;
|
|
16794
|
+
* the consumer should create a new instance.
|
|
16795
|
+
*/
|
|
16796
|
+
get isClosed() {
|
|
16797
|
+
return this._closed;
|
|
16798
|
+
}
|
|
16799
|
+
/**
|
|
16800
|
+
* `true` when the API is usable: not closed, general socket exists, socket
|
|
16801
|
+
* is connected and the client is logged in.
|
|
16802
|
+
*
|
|
16803
|
+
* This is the recommended way for consumers to check whether the API is
|
|
16804
|
+
* still valid before issuing commands, instead of directly accessing
|
|
16805
|
+
* `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
|
|
16806
|
+
* if the socket pool was already destroyed).
|
|
16807
|
+
*/
|
|
16808
|
+
get isReady() {
|
|
16809
|
+
if (this._closed) return false;
|
|
16810
|
+
const entry = this.socketPool.get("general");
|
|
16811
|
+
if (!entry) return false;
|
|
16812
|
+
try {
|
|
16813
|
+
return entry.client.isSocketConnected() && entry.client.loggedIn;
|
|
16814
|
+
} catch {
|
|
16815
|
+
return false;
|
|
16816
|
+
}
|
|
16817
|
+
}
|
|
16818
|
+
/** Promise tracking an in-flight reconnection from `ensureConnected()`. */
|
|
16819
|
+
_ensureConnectedPromise;
|
|
16820
|
+
/**
|
|
16821
|
+
* Ensure the "general" socket is connected and logged in.
|
|
16822
|
+
* If the socket is disconnected or the pool entry was destroyed, a new
|
|
16823
|
+
* general socket is created, logged in, and all event/push/guard listeners
|
|
16824
|
+
* are re-attached automatically.
|
|
16825
|
+
*
|
|
16826
|
+
* This is a **no-op** when the API is already {@link isReady}.
|
|
16827
|
+
*
|
|
16828
|
+
* @throws If `close()` was called — the API is permanently closed and a new
|
|
16829
|
+
* instance must be created.
|
|
16830
|
+
*/
|
|
16831
|
+
async ensureConnected() {
|
|
16832
|
+
if (this._closed) {
|
|
16833
|
+
throw new Error(
|
|
16834
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
16835
|
+
);
|
|
16836
|
+
}
|
|
16837
|
+
if (this.isReady) return;
|
|
16838
|
+
if (this._ensureConnectedPromise) {
|
|
16839
|
+
return this._ensureConnectedPromise;
|
|
16840
|
+
}
|
|
16841
|
+
this._ensureConnectedPromise = this.reconnectGeneralSocket();
|
|
16842
|
+
try {
|
|
16843
|
+
await this._ensureConnectedPromise;
|
|
16844
|
+
} finally {
|
|
16845
|
+
this._ensureConnectedPromise = void 0;
|
|
16846
|
+
}
|
|
16847
|
+
}
|
|
16848
|
+
/**
|
|
16849
|
+
* Internal: destroy the current general socket (if any), create a new one,
|
|
16850
|
+
* login, and re-attach all listeners.
|
|
16851
|
+
*/
|
|
16852
|
+
async reconnectGeneralSocket() {
|
|
16853
|
+
const oldEntry = this.socketPool.get("general");
|
|
16854
|
+
if (oldEntry) {
|
|
16855
|
+
oldEntry.client.removeAllListeners();
|
|
16856
|
+
if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
|
|
16857
|
+
if (oldEntry.generalPermitRelease) {
|
|
16858
|
+
try {
|
|
16859
|
+
oldEntry.generalPermitRelease();
|
|
16860
|
+
} catch {
|
|
16861
|
+
}
|
|
16862
|
+
}
|
|
16863
|
+
this.socketPool.delete("general");
|
|
16864
|
+
try {
|
|
16865
|
+
await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
|
|
16866
|
+
} catch {
|
|
16867
|
+
}
|
|
16868
|
+
}
|
|
16869
|
+
const newClient = new BaichuanClient(this.clientOptions);
|
|
16870
|
+
this.socketPool.set("general", {
|
|
16871
|
+
client: newClient,
|
|
16872
|
+
refCount: 1,
|
|
16873
|
+
// general socket is always "in use"
|
|
16874
|
+
createdAt: Date.now(),
|
|
16875
|
+
lastUsedAt: Date.now(),
|
|
16876
|
+
idleCloseTimer: void 0,
|
|
16877
|
+
generalPermitRelease: void 0
|
|
16878
|
+
});
|
|
16879
|
+
this.setupGeneralClientListeners();
|
|
16880
|
+
await this.client.login();
|
|
16881
|
+
this.logger.log?.(
|
|
16882
|
+
"[ReolinkBaichuanApi] General socket reconnected successfully"
|
|
16883
|
+
);
|
|
16884
|
+
if (this.simpleEventListeners.size > 0) {
|
|
16885
|
+
this.simpleEventSubscribed = false;
|
|
16886
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
16887
|
+
this.simpleEventWatchdogLastRecoveryAt = 0;
|
|
16888
|
+
try {
|
|
16889
|
+
await this.ensureSimpleEventSubscribed();
|
|
16890
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
16891
|
+
this.logger.log?.(
|
|
16892
|
+
`[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
|
|
16893
|
+
);
|
|
16894
|
+
} catch (e) {
|
|
16895
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
16896
|
+
this.logger,
|
|
16897
|
+
`[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
|
|
16898
|
+
formatErrorForLog(e)
|
|
16899
|
+
);
|
|
16900
|
+
}
|
|
16901
|
+
}
|
|
16902
|
+
}
|
|
16903
|
+
/**
|
|
16904
|
+
* Attach event, push, channelInfo, and guard listeners to the current
|
|
16905
|
+
* "general" client. Called from the constructor and from
|
|
16906
|
+
* {@link reconnectGeneralSocket}.
|
|
16907
|
+
*/
|
|
16908
|
+
setupGeneralClientListeners() {
|
|
16909
|
+
const client = this.client;
|
|
16910
|
+
client.on("event", (event) => {
|
|
16911
|
+
const mapped = mapToSimpleEvent(event);
|
|
16912
|
+
if (!mapped) return;
|
|
16913
|
+
this.dispatchSimpleEvent(mapped);
|
|
16914
|
+
});
|
|
16915
|
+
client.on("channelInfo", (xml) => {
|
|
16916
|
+
try {
|
|
16917
|
+
this.parseAndStoreChannelInfo(xml);
|
|
16918
|
+
} catch (e) {
|
|
16919
|
+
this.logger.warn?.(
|
|
16920
|
+
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
16921
|
+
formatErrorForLog(e)
|
|
16922
|
+
);
|
|
16923
|
+
}
|
|
16924
|
+
});
|
|
16925
|
+
client.on("push", (frame) => {
|
|
16926
|
+
const cmdId = frame.header.cmdId;
|
|
16927
|
+
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) {
|
|
16928
|
+
return;
|
|
16929
|
+
}
|
|
16930
|
+
try {
|
|
16931
|
+
if (frame.body.length === 0) return;
|
|
16932
|
+
const xml = client.tryDecryptXml(
|
|
16933
|
+
frame.body,
|
|
16934
|
+
frame.header.channelId,
|
|
16935
|
+
client.enc
|
|
16936
|
+
);
|
|
16937
|
+
if (!xml || !xml.startsWith("<?xml")) return;
|
|
16938
|
+
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
16939
|
+
} catch (e) {
|
|
16940
|
+
this.logger.debug?.(
|
|
16941
|
+
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
16942
|
+
formatErrorForLog(e)
|
|
16943
|
+
);
|
|
16944
|
+
}
|
|
16945
|
+
});
|
|
16946
|
+
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
16947
|
+
client.on("close", () => {
|
|
16948
|
+
try {
|
|
16949
|
+
void this.maybeRebootOnDisconnectStorm();
|
|
16950
|
+
} catch {
|
|
16951
|
+
}
|
|
16952
|
+
});
|
|
16953
|
+
}
|
|
16954
|
+
if (this.rebootAfterConsecutiveEconnreset > 0) {
|
|
16955
|
+
client.on("close", () => {
|
|
16956
|
+
try {
|
|
16957
|
+
void this.maybeRebootOnEconnresetStorm();
|
|
16958
|
+
} catch {
|
|
16959
|
+
}
|
|
16960
|
+
});
|
|
16961
|
+
}
|
|
16962
|
+
if (!this.sessionGuardIntervalTimer) {
|
|
16963
|
+
client.once("push", () => {
|
|
16964
|
+
void this.logActiveSessionsOnStartup();
|
|
16965
|
+
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
16966
|
+
void this.maybeRebootOnTooManySessions();
|
|
16967
|
+
}, 6e4);
|
|
16968
|
+
});
|
|
16969
|
+
}
|
|
16970
|
+
}
|
|
16499
16971
|
/**
|
|
16500
16972
|
* Cached camera UID. May be initially undefined if not provided in the constructor.
|
|
16501
16973
|
* Will be lazily populated on demand when needed (e.g. for recordings).
|
|
@@ -17436,42 +17908,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17436
17908
|
logger: this.logger,
|
|
17437
17909
|
debugConfig: generalClient.getDebugConfig?.()
|
|
17438
17910
|
});
|
|
17439
|
-
this.client.on("event", (event) => {
|
|
17440
|
-
const mapped = mapToSimpleEvent(event);
|
|
17441
|
-
if (!mapped) return;
|
|
17442
|
-
this.dispatchSimpleEvent(mapped);
|
|
17443
|
-
});
|
|
17444
|
-
this.client.on("channelInfo", (xml) => {
|
|
17445
|
-
try {
|
|
17446
|
-
this.parseAndStoreChannelInfo(xml);
|
|
17447
|
-
} catch (e) {
|
|
17448
|
-
this.logger.warn?.(
|
|
17449
|
-
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
17450
|
-
formatErrorForLog(e)
|
|
17451
|
-
);
|
|
17452
|
-
}
|
|
17453
|
-
});
|
|
17454
|
-
this.client.on("push", (frame) => {
|
|
17455
|
-
const cmdId = frame.header.cmdId;
|
|
17456
|
-
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) {
|
|
17457
|
-
return;
|
|
17458
|
-
}
|
|
17459
|
-
try {
|
|
17460
|
-
if (frame.body.length === 0) return;
|
|
17461
|
-
const xml = this.client.tryDecryptXml(
|
|
17462
|
-
frame.body,
|
|
17463
|
-
frame.header.channelId,
|
|
17464
|
-
this.client.enc
|
|
17465
|
-
);
|
|
17466
|
-
if (!xml || !xml.startsWith("<?xml")) return;
|
|
17467
|
-
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
17468
|
-
} catch (e) {
|
|
17469
|
-
this.logger.debug?.(
|
|
17470
|
-
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
17471
|
-
formatErrorForLog(e)
|
|
17472
|
-
);
|
|
17473
|
-
}
|
|
17474
|
-
});
|
|
17475
17911
|
const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
|
|
17476
17912
|
if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
|
|
17477
17913
|
this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
|
|
@@ -17480,32 +17916,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17480
17916
|
if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
|
|
17481
17917
|
this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
|
|
17482
17918
|
}
|
|
17483
|
-
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
17484
|
-
this.client.on("close", () => {
|
|
17485
|
-
try {
|
|
17486
|
-
void this.maybeRebootOnDisconnectStorm();
|
|
17487
|
-
} catch {
|
|
17488
|
-
}
|
|
17489
|
-
});
|
|
17490
|
-
}
|
|
17491
17919
|
const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
|
|
17492
17920
|
if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
|
|
17493
17921
|
this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
|
|
17494
17922
|
}
|
|
17495
|
-
|
|
17496
|
-
this.client.on("close", () => {
|
|
17497
|
-
try {
|
|
17498
|
-
void this.maybeRebootOnEconnresetStorm();
|
|
17499
|
-
} catch {
|
|
17500
|
-
}
|
|
17501
|
-
});
|
|
17502
|
-
}
|
|
17503
|
-
this.client.once("push", () => {
|
|
17504
|
-
void this.logActiveSessionsOnStartup();
|
|
17505
|
-
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
17506
|
-
void this.maybeRebootOnTooManySessions();
|
|
17507
|
-
}, 6e4);
|
|
17508
|
-
});
|
|
17923
|
+
this.setupGeneralClientListeners();
|
|
17509
17924
|
}
|
|
17510
17925
|
/**
|
|
17511
17926
|
* CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
|
|
@@ -18336,6 +18751,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18336
18751
|
);
|
|
18337
18752
|
}
|
|
18338
18753
|
async close(options) {
|
|
18754
|
+
if (this._closed) return;
|
|
18755
|
+
this._closed = true;
|
|
18339
18756
|
if (this.sessionGuardIntervalTimer) {
|
|
18340
18757
|
clearInterval(this.sessionGuardIntervalTimer);
|
|
18341
18758
|
this.sessionGuardIntervalTimer = void 0;
|
|
@@ -18398,7 +18815,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18398
18815
|
}
|
|
18399
18816
|
async handleSendXml400(params, frame, retry) {
|
|
18400
18817
|
const emptyBody = frame.body.length === 0;
|
|
18401
|
-
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes:
|
|
18818
|
+
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
|
|
18402
18819
|
if (this.isSendXmlFailFast400(params, frame.body.length)) {
|
|
18403
18820
|
throw new Error(emptyBody400Msg);
|
|
18404
18821
|
}
|
|
@@ -18914,11 +19331,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18914
19331
|
* Minimal per-channel inventory for NVR-connected devices.
|
|
18915
19332
|
*
|
|
18916
19333
|
* Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
|
|
19334
|
+
*
|
|
19335
|
+
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
19336
|
+
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
19337
|
+
* no dependency on async push messages. Recommended for first-call discovery.
|
|
19338
|
+
* - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
|
|
19339
|
+
* info after login + event subscription. This push is *asynchronous*: if it has not
|
|
19340
|
+
* arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
|
|
19341
|
+
* with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
|
|
18917
19342
|
*/
|
|
18918
19343
|
async getNvrChannelsSummary(options) {
|
|
18919
|
-
const source = options?.source ?? "
|
|
18920
|
-
|
|
18921
|
-
const
|
|
19344
|
+
const source = options?.source ?? "cgi";
|
|
19345
|
+
let channels;
|
|
19346
|
+
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
19347
|
+
if (options?.channels?.length) {
|
|
19348
|
+
channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
19349
|
+
} else if (source === "cgi") {
|
|
19350
|
+
try {
|
|
19351
|
+
const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
|
|
19352
|
+
const status = channelsResponse?.[0]?.value?.status ?? [];
|
|
19353
|
+
for (const s of status) {
|
|
19354
|
+
const ch = Number(s?.channel);
|
|
19355
|
+
if (!Number.isFinite(ch)) continue;
|
|
19356
|
+
cgiStatusByChannel.set(ch, {
|
|
19357
|
+
...s.name != null ? { name: s.name } : {},
|
|
19358
|
+
...s.uid != null ? { uid: s.uid } : {},
|
|
19359
|
+
sleeping: s.sleep === 1
|
|
19360
|
+
});
|
|
19361
|
+
}
|
|
19362
|
+
channels = cgiChannels;
|
|
19363
|
+
this.logger.debug?.(
|
|
19364
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
19365
|
+
);
|
|
19366
|
+
} catch (e) {
|
|
19367
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19368
|
+
this.logger.warn?.(
|
|
19369
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
|
|
19370
|
+
);
|
|
19371
|
+
channels = [];
|
|
19372
|
+
}
|
|
19373
|
+
} else {
|
|
19374
|
+
const pushInfo2 = this.getChannelInfoFromPushCache();
|
|
19375
|
+
channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
19376
|
+
}
|
|
19377
|
+
channels = channels.sort((a, b) => a - b);
|
|
18922
19378
|
const support = await this.getSupportInfo().catch(() => {
|
|
18923
19379
|
this.logger.error?.(
|
|
18924
19380
|
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
@@ -18948,7 +19404,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18948
19404
|
);
|
|
18949
19405
|
}
|
|
18950
19406
|
}
|
|
18951
|
-
const cacheKey =
|
|
19407
|
+
const cacheKey = `${source}:${channels.join(",")}`;
|
|
18952
19408
|
const cached = this.nvrChannelsSummaryCache.get(cacheKey);
|
|
18953
19409
|
if (cached) {
|
|
18954
19410
|
return {
|
|
@@ -18969,8 +19425,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18969
19425
|
} catch {
|
|
18970
19426
|
}
|
|
18971
19427
|
}
|
|
19428
|
+
const pushInfo = this.getChannelInfoFromPushCache();
|
|
18972
19429
|
const devices = channels.map((channel) => {
|
|
18973
|
-
const
|
|
19430
|
+
const pushCached = pushInfo.get(channel);
|
|
19431
|
+
const cgiStatus = cgiStatusByChannel.get(channel);
|
|
18974
19432
|
const info = infoPerChannel.get(channel);
|
|
18975
19433
|
const networkInfo = networkInfoPerChannel.get(channel);
|
|
18976
19434
|
const isBattery = isBatteryByChannel.get(channel) ?? false;
|
|
@@ -18978,6 +19436,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18978
19436
|
const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
|
|
18979
19437
|
const normalizedModel = model ? model.trim() : void 0;
|
|
18980
19438
|
const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
19439
|
+
const name = pushCached?.name || cgiStatus?.name || "";
|
|
19440
|
+
const uid = pushCached?.uid || cgiStatus?.uid || "";
|
|
19441
|
+
const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
|
|
18981
19442
|
return {
|
|
18982
19443
|
channel,
|
|
18983
19444
|
isBattery,
|
|
@@ -18987,19 +19448,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18987
19448
|
...networkInfo?.ip ? { ip: networkInfo.ip } : {},
|
|
18988
19449
|
...networkInfo?.mac ? { mac: networkInfo.mac } : {},
|
|
18989
19450
|
...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
|
|
18990
|
-
...
|
|
18991
|
-
...
|
|
18992
|
-
...
|
|
18993
|
-
...typeof
|
|
18994
|
-
...
|
|
18995
|
-
...
|
|
18996
|
-
...
|
|
18997
|
-
...typeof
|
|
18998
|
-
...typeof
|
|
18999
|
-
...typeof
|
|
19000
|
-
...typeof
|
|
19001
|
-
...
|
|
19002
|
-
...typeof
|
|
19451
|
+
...name ? { name } : {},
|
|
19452
|
+
...uid ? { uid } : {},
|
|
19453
|
+
...pushCached?.state ? { state: pushCached.state } : {},
|
|
19454
|
+
...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
|
|
19455
|
+
...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
|
|
19456
|
+
...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
|
|
19457
|
+
...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
|
|
19458
|
+
...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
|
|
19459
|
+
...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
|
|
19460
|
+
...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
|
|
19461
|
+
...typeof sleeping === "boolean" ? { sleeping } : {},
|
|
19462
|
+
...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
|
|
19463
|
+
...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
|
|
19003
19464
|
};
|
|
19004
19465
|
});
|
|
19005
19466
|
const result = { channels, devices };
|
|
@@ -23267,13 +23728,12 @@ ${xml}`
|
|
|
23267
23728
|
]);
|
|
23268
23729
|
const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
|
|
23269
23730
|
const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
|
|
23270
|
-
const supportItem =
|
|
23271
|
-
const capabilities =
|
|
23272
|
-
ch,
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
);
|
|
23731
|
+
const supportItem = getSupportItemForChannel(support, ch);
|
|
23732
|
+
const capabilities = computeDeviceCapabilities({
|
|
23733
|
+
channel: ch,
|
|
23734
|
+
...support != null && { support },
|
|
23735
|
+
...abilities != null && { abilities }
|
|
23736
|
+
});
|
|
23277
23737
|
const item = supportItem;
|
|
23278
23738
|
const lightType = item?.lightType;
|
|
23279
23739
|
const ledCtrl = item?.ledCtrl;
|
|
@@ -23289,6 +23749,25 @@ ${xml}`
|
|
|
23289
23749
|
});
|
|
23290
23750
|
capabilities.hasFloodlight = probed;
|
|
23291
23751
|
}
|
|
23752
|
+
let dingDongListIds;
|
|
23753
|
+
let dingDongCfgIds;
|
|
23754
|
+
let wirelessChimeError;
|
|
23755
|
+
if (capabilities.hasWirelessChime) {
|
|
23756
|
+
try {
|
|
23757
|
+
const list = await this.getDingDongList(ch);
|
|
23758
|
+
dingDongListIds = list.map((d) => d.id);
|
|
23759
|
+
const first = list[0];
|
|
23760
|
+
const fromList = first !== void 0 && first.id >= 0;
|
|
23761
|
+
if (!fromList) {
|
|
23762
|
+
const configs = await this.getDingDongCfg(ch);
|
|
23763
|
+
dingDongCfgIds = configs.map((c) => c.id);
|
|
23764
|
+
capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
|
|
23765
|
+
}
|
|
23766
|
+
} catch (e) {
|
|
23767
|
+
capabilities.hasWirelessChime = false;
|
|
23768
|
+
wirelessChimeError = e instanceof Error ? e.message : String(e);
|
|
23769
|
+
}
|
|
23770
|
+
}
|
|
23292
23771
|
const features = this.parseFeaturesFromSupport(support);
|
|
23293
23772
|
const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
|
|
23294
23773
|
const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
|
|
@@ -23325,7 +23804,10 @@ ${xml}`
|
|
|
23325
23804
|
...abilities && {
|
|
23326
23805
|
abilityMergedKeyCount: Object.keys(abilities).length
|
|
23327
23806
|
},
|
|
23328
|
-
...support?.items && { supportItemCount: support.items.length }
|
|
23807
|
+
...support?.items && { supportItemCount: support.items.length },
|
|
23808
|
+
...dingDongListIds !== void 0 && { dingDongListIds },
|
|
23809
|
+
...dingDongCfgIds !== void 0 && { dingDongCfgIds },
|
|
23810
|
+
...wirelessChimeError !== void 0 && { wirelessChimeError }
|
|
23329
23811
|
};
|
|
23330
23812
|
const result = {
|
|
23331
23813
|
capabilities,
|
|
@@ -23352,90 +23834,6 @@ ${xml}`
|
|
|
23352
23834
|
this.deviceCapabilitiesCache.clear();
|
|
23353
23835
|
}
|
|
23354
23836
|
}
|
|
23355
|
-
/**
|
|
23356
|
-
* Pick the best SupportItem for a channel.
|
|
23357
|
-
* Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
|
|
23358
|
-
*/
|
|
23359
|
-
pickBestSupportItem(support, channel) {
|
|
23360
|
-
if (!support?.items?.length) return void 0;
|
|
23361
|
-
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
23362
|
-
if (!candidates.length) return void 0;
|
|
23363
|
-
const score = (item) => {
|
|
23364
|
-
const anyItem = item;
|
|
23365
|
-
let result = 0;
|
|
23366
|
-
if (anyItem.name == null) result += 100;
|
|
23367
|
-
const capabilityKeys = [
|
|
23368
|
-
"ptzType",
|
|
23369
|
-
"ptzControl",
|
|
23370
|
-
"ptzPreset",
|
|
23371
|
-
"ledCtrl",
|
|
23372
|
-
"lightType",
|
|
23373
|
-
"battery",
|
|
23374
|
-
"audioVersion",
|
|
23375
|
-
"motion",
|
|
23376
|
-
"encCtrl",
|
|
23377
|
-
"newIspCfg",
|
|
23378
|
-
"remoteAbility",
|
|
23379
|
-
"aitype",
|
|
23380
|
-
"videoClip",
|
|
23381
|
-
"snap"
|
|
23382
|
-
];
|
|
23383
|
-
for (const k of capabilityKeys) {
|
|
23384
|
-
if (anyItem[k] !== void 0) result += 3;
|
|
23385
|
-
}
|
|
23386
|
-
return result;
|
|
23387
|
-
};
|
|
23388
|
-
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
23389
|
-
}
|
|
23390
|
-
/**
|
|
23391
|
-
* Parse device capabilities from SupportInfo.
|
|
23392
|
-
* Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
|
|
23393
|
-
*/
|
|
23394
|
-
parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
|
|
23395
|
-
const truthy = (v) => {
|
|
23396
|
-
if (typeof v === "number") return v > 0;
|
|
23397
|
-
if (typeof v === "string") {
|
|
23398
|
-
const n = Number(v);
|
|
23399
|
-
return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
|
|
23400
|
-
}
|
|
23401
|
-
return Boolean(v);
|
|
23402
|
-
};
|
|
23403
|
-
const item = supportItem;
|
|
23404
|
-
const ptzMode = support?.ptzMode?.toLowerCase();
|
|
23405
|
-
const ptzType = item ? truthy(item.ptzType) : false;
|
|
23406
|
-
const ptzControl = item ? truthy(item.ptzControl) : false;
|
|
23407
|
-
const hasPtzFromItem = ptzType || ptzControl;
|
|
23408
|
-
const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
|
|
23409
|
-
const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
|
|
23410
|
-
const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
|
|
23411
|
-
const hasPresets = item ? truthy(item.ptzPreset) : false;
|
|
23412
|
-
const hasBattery = item ? truthy(item.battery) : false;
|
|
23413
|
-
const hasSiren = item ? truthy(item.audioVersion) : false;
|
|
23414
|
-
const lightType = item?.lightType;
|
|
23415
|
-
const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
|
|
23416
|
-
const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
|
|
23417
|
-
const isDoorbell = item ? truthy(item.doorbellVersion) : false;
|
|
23418
|
-
const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
|
|
23419
|
-
return {
|
|
23420
|
-
channel,
|
|
23421
|
-
...ptzMode && { ptzMode },
|
|
23422
|
-
hasPan: hasPanTilt,
|
|
23423
|
-
hasTilt: hasPanTilt,
|
|
23424
|
-
hasZoom,
|
|
23425
|
-
hasPresets,
|
|
23426
|
-
hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
|
|
23427
|
-
hasBattery,
|
|
23428
|
-
hasIntercom,
|
|
23429
|
-
hasSiren,
|
|
23430
|
-
hasFloodlight,
|
|
23431
|
-
hasPir,
|
|
23432
|
-
isDoorbell,
|
|
23433
|
-
// Autotracking: explicit flags only (autoPt or smartAI)
|
|
23434
|
-
// Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
|
|
23435
|
-
// on cameras that have PTZ and AI detection but NOT autotracking capability.
|
|
23436
|
-
hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
|
|
23437
|
-
};
|
|
23438
|
-
}
|
|
23439
23837
|
/**
|
|
23440
23838
|
* Parse support features from SupportInfo.
|
|
23441
23839
|
*/
|
|
@@ -26308,6 +26706,216 @@ ${scheduleItems}
|
|
|
26308
26706
|
const channel = 0;
|
|
26309
26707
|
return await this.getSnapshot(channel);
|
|
26310
26708
|
}
|
|
26709
|
+
// --------------------
|
|
26710
|
+
// Chime / DingDong APIs
|
|
26711
|
+
// --------------------
|
|
26712
|
+
/**
|
|
26713
|
+
* Get the list of paired wireless chime devices.
|
|
26714
|
+
* cmd_id: 484 (GetDingDongList)
|
|
26715
|
+
*
|
|
26716
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26717
|
+
* @returns Array of paired chime devices
|
|
26718
|
+
*/
|
|
26719
|
+
async getDingDongList(channel) {
|
|
26720
|
+
const ch = this.normalizeChannel(channel);
|
|
26721
|
+
const xml = await this.sendXml({
|
|
26722
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
|
|
26723
|
+
channel: ch
|
|
26724
|
+
});
|
|
26725
|
+
return parseDingDongListFromXml(xml);
|
|
26726
|
+
}
|
|
26727
|
+
/**
|
|
26728
|
+
* Get parameters (name, volume, LED state) for a specific wireless chime.
|
|
26729
|
+
* cmd_id: 485 (DingDongOpt, option getParam)
|
|
26730
|
+
*
|
|
26731
|
+
* @param chimeId - The chime device ID
|
|
26732
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26733
|
+
* @returns Chime parameters
|
|
26734
|
+
*/
|
|
26735
|
+
async getDingDongParams(chimeId, channel) {
|
|
26736
|
+
const ch = this.normalizeChannel(channel);
|
|
26737
|
+
const payloadXml = buildDingDongGetParamsXml(chimeId);
|
|
26738
|
+
const xml = await this.sendXml({
|
|
26739
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
26740
|
+
channel: ch,
|
|
26741
|
+
payloadXml
|
|
26742
|
+
});
|
|
26743
|
+
return parseDingDongParamsFromXml(xml);
|
|
26744
|
+
}
|
|
26745
|
+
/**
|
|
26746
|
+
* Set parameters (name, volume, LED state) for a specific wireless chime.
|
|
26747
|
+
* cmd_id: 485 (DingDongOpt, option setParam)
|
|
26748
|
+
*
|
|
26749
|
+
* @param chimeId - The chime device ID
|
|
26750
|
+
* @param params - Parameters to set (volLevel, ledState, name)
|
|
26751
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26752
|
+
*/
|
|
26753
|
+
async setDingDongParams(chimeId, params, channel) {
|
|
26754
|
+
const ch = this.normalizeChannel(channel);
|
|
26755
|
+
const payloadXml = buildDingDongSetParamsXml(chimeId, params);
|
|
26756
|
+
await this.sendXml({
|
|
26757
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
26758
|
+
channel: ch,
|
|
26759
|
+
payloadXml
|
|
26760
|
+
});
|
|
26761
|
+
}
|
|
26762
|
+
/**
|
|
26763
|
+
* Trigger a wireless chime to ring with a specific ringtone.
|
|
26764
|
+
* cmd_id: 485 (DingDongOpt, option ringWithMusic)
|
|
26765
|
+
*
|
|
26766
|
+
* @param chimeId - The chime device ID
|
|
26767
|
+
* @param musicId - The ringtone/music ID to play
|
|
26768
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26769
|
+
*/
|
|
26770
|
+
async ringDingDong(chimeId, musicId, channel) {
|
|
26771
|
+
const ch = this.normalizeChannel(channel);
|
|
26772
|
+
const payloadXml = buildDingDongRingXml(chimeId, musicId);
|
|
26773
|
+
await this.sendXml({
|
|
26774
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
26775
|
+
channel: ch,
|
|
26776
|
+
payloadXml
|
|
26777
|
+
});
|
|
26778
|
+
}
|
|
26779
|
+
/**
|
|
26780
|
+
* Get the per-event alarm configuration for paired wireless chimes.
|
|
26781
|
+
* cmd_id: 486 (GetDingDongCfg)
|
|
26782
|
+
*
|
|
26783
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26784
|
+
* @returns Array of chime configurations (one per paired chime)
|
|
26785
|
+
*/
|
|
26786
|
+
async getDingDongCfg(channel) {
|
|
26787
|
+
const ch = this.normalizeChannel(channel);
|
|
26788
|
+
const xml = await this.sendXml({
|
|
26789
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
|
|
26790
|
+
channel: ch
|
|
26791
|
+
});
|
|
26792
|
+
return parseDingDongCfgFromXml(xml);
|
|
26793
|
+
}
|
|
26794
|
+
/**
|
|
26795
|
+
* Set the per-event alarm configuration for a specific wireless chime.
|
|
26796
|
+
* cmd_id: 487 (SetDingDongCfg)
|
|
26797
|
+
*
|
|
26798
|
+
* @param chimeId - The chime ring/device ID
|
|
26799
|
+
* @param eventType - Event type string (e.g. "doorbell", "package", "people")
|
|
26800
|
+
* @param state - 0 = disabled, 1 = enabled
|
|
26801
|
+
* @param musicId - Ringtone ID to use for this event type
|
|
26802
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26803
|
+
*/
|
|
26804
|
+
async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
|
|
26805
|
+
const ch = this.normalizeChannel(channel);
|
|
26806
|
+
const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
|
|
26807
|
+
await this.sendXml({
|
|
26808
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
|
|
26809
|
+
channel: ch,
|
|
26810
|
+
payloadXml
|
|
26811
|
+
});
|
|
26812
|
+
}
|
|
26813
|
+
/** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
|
|
26814
|
+
_hardwiredChimeCache = /* @__PURE__ */ new Map();
|
|
26815
|
+
/**
|
|
26816
|
+
* Get the hardwired (wired-in) chime state.
|
|
26817
|
+
* cmd_id: 483 (GetDingDongCtrl)
|
|
26818
|
+
*
|
|
26819
|
+
* Note: calling this may briefly trigger the physical chime to rattle.
|
|
26820
|
+
*
|
|
26821
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26822
|
+
* @returns Hardwired chime state (type, enabled, time)
|
|
26823
|
+
*/
|
|
26824
|
+
async getHardwiredChime(channel) {
|
|
26825
|
+
const ch = this.normalizeChannel(channel);
|
|
26826
|
+
const payloadXml = buildGetDingDongCtrlXml();
|
|
26827
|
+
const xml = await this.sendXml({
|
|
26828
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
26829
|
+
channel: ch,
|
|
26830
|
+
payloadXml
|
|
26831
|
+
});
|
|
26832
|
+
const state = parseHardwiredChimeFromXml(xml);
|
|
26833
|
+
this._hardwiredChimeCache.set(ch, state);
|
|
26834
|
+
return state;
|
|
26835
|
+
}
|
|
26836
|
+
/**
|
|
26837
|
+
* Set the hardwired (wired-in) chime state.
|
|
26838
|
+
* cmd_id: 483 (SetDingDongCtrl)
|
|
26839
|
+
*
|
|
26840
|
+
* Uses the cached state from a previous getHardwiredChime call to fill in
|
|
26841
|
+
* missing type/time fields, avoiding a double round-trip on every set.
|
|
26842
|
+
* Falls back to fetching if no cache is available.
|
|
26843
|
+
*
|
|
26844
|
+
* @param params - Chime configuration (type, enabled, time)
|
|
26845
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26846
|
+
*/
|
|
26847
|
+
async setHardwiredChime(params, channel) {
|
|
26848
|
+
const ch = this.normalizeChannel(channel);
|
|
26849
|
+
let current = this._hardwiredChimeCache.get(ch);
|
|
26850
|
+
if (!current) {
|
|
26851
|
+
current = await this.getHardwiredChime(ch);
|
|
26852
|
+
}
|
|
26853
|
+
const chimeType = params.type ?? current.type;
|
|
26854
|
+
const enabled = params.enabled ? 1 : 0;
|
|
26855
|
+
const time = params.time ?? current.time;
|
|
26856
|
+
const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
|
|
26857
|
+
const xml = await this.sendXml({
|
|
26858
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
26859
|
+
channel: ch,
|
|
26860
|
+
payloadXml
|
|
26861
|
+
});
|
|
26862
|
+
const newState = parseHardwiredChimeFromXml(xml);
|
|
26863
|
+
this._hardwiredChimeCache.set(ch, newState);
|
|
26864
|
+
return newState;
|
|
26865
|
+
}
|
|
26866
|
+
/**
|
|
26867
|
+
* Play an audio file on the doorbell / chime device.
|
|
26868
|
+
* cmd_id: 349 (QuickReplyPlay)
|
|
26869
|
+
*
|
|
26870
|
+
* @param fileId - The audio file ID to play
|
|
26871
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26872
|
+
*/
|
|
26873
|
+
async quickReplyPlay(fileId, channel) {
|
|
26874
|
+
const ch = this.normalizeChannel(channel);
|
|
26875
|
+
const payloadXml = buildQuickReplyPlayXml(ch, fileId);
|
|
26876
|
+
await this.sendXml({
|
|
26877
|
+
cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
26878
|
+
channel: ch,
|
|
26879
|
+
payloadXml
|
|
26880
|
+
});
|
|
26881
|
+
}
|
|
26882
|
+
/**
|
|
26883
|
+
* Get the silent mode state of a paired wireless chime.
|
|
26884
|
+
* cmd_id: 609 (GetDingDongSilent)
|
|
26885
|
+
*
|
|
26886
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
26887
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26888
|
+
* @returns Wireless chime silent state (time=0 means active/not silenced)
|
|
26889
|
+
*/
|
|
26890
|
+
async getDingDongSilent(chimeId, channel) {
|
|
26891
|
+
const ch = this.normalizeChannel(channel);
|
|
26892
|
+
const payloadXml = buildGetDingDongSilentXml(chimeId);
|
|
26893
|
+
const xml = await this.sendXml({
|
|
26894
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
26895
|
+
channel: ch,
|
|
26896
|
+
payloadXml
|
|
26897
|
+
});
|
|
26898
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
26899
|
+
}
|
|
26900
|
+
/**
|
|
26901
|
+
* Set the silent mode of a paired wireless chime.
|
|
26902
|
+
* cmd_id: 610 (SetDingDongSilent)
|
|
26903
|
+
*
|
|
26904
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
26905
|
+
* @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
|
|
26906
|
+
* @param channel - Channel number (0-based, default 0)
|
|
26907
|
+
* @returns Updated wireless chime silent state
|
|
26908
|
+
*/
|
|
26909
|
+
async setDingDongSilent(chimeId, time, channel) {
|
|
26910
|
+
const ch = this.normalizeChannel(channel);
|
|
26911
|
+
const payloadXml = buildSetDingDongSilentXml(chimeId, time);
|
|
26912
|
+
const xml = await this.sendXml({
|
|
26913
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
26914
|
+
channel: ch,
|
|
26915
|
+
payloadXml
|
|
26916
|
+
});
|
|
26917
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
26918
|
+
}
|
|
26311
26919
|
};
|
|
26312
26920
|
|
|
26313
26921
|
// src/reolink/autodetect.ts
|