@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/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
function bcHeaderHasPayloadOffset(messageClass) {
|
|
35
35
|
return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
|
|
36
36
|
}
|
|
37
|
-
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_LOGIN, 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_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, 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_ALARM_EVENT_LIST, 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;
|
|
37
|
+
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_LOGIN, 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_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, 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_ALARM_EVENT_LIST, 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;
|
|
38
38
|
var init_constants = __esm({
|
|
39
39
|
"src/protocol/constants.ts"() {
|
|
40
40
|
"use strict";
|
|
@@ -147,6 +147,14 @@ var init_constants = __esm({
|
|
|
147
147
|
BC_CMD_ID_PUSH_DINGDONG_LIST = 484;
|
|
148
148
|
BC_CMD_ID_PUSH_SLEEP_STATUS = 623;
|
|
149
149
|
BC_CMD_ID_PUSH_COORDINATE_POINT_LIST = 723;
|
|
150
|
+
BC_CMD_ID_DING_DONG_CTRL = 483;
|
|
151
|
+
BC_CMD_ID_GET_DING_DONG_LIST = 484;
|
|
152
|
+
BC_CMD_ID_DING_DONG_OPT = 485;
|
|
153
|
+
BC_CMD_ID_GET_DING_DONG_CFG = 486;
|
|
154
|
+
BC_CMD_ID_SET_DING_DONG_CFG = 487;
|
|
155
|
+
BC_CMD_ID_QUICK_REPLY_PLAY = 349;
|
|
156
|
+
BC_CMD_ID_GET_DING_DONG_SILENT = 609;
|
|
157
|
+
BC_CMD_ID_SET_DING_DONG_SILENT = 610;
|
|
150
158
|
}
|
|
151
159
|
});
|
|
152
160
|
|
|
@@ -7331,6 +7339,8 @@ __export(index_exports, {
|
|
|
7331
7339
|
BC_CMD_ID_COVER_STANDALONE_460: () => BC_CMD_ID_COVER_STANDALONE_460,
|
|
7332
7340
|
BC_CMD_ID_COVER_STANDALONE_461: () => BC_CMD_ID_COVER_STANDALONE_461,
|
|
7333
7341
|
BC_CMD_ID_COVER_STANDALONE_462: () => BC_CMD_ID_COVER_STANDALONE_462,
|
|
7342
|
+
BC_CMD_ID_DING_DONG_CTRL: () => BC_CMD_ID_DING_DONG_CTRL,
|
|
7343
|
+
BC_CMD_ID_DING_DONG_OPT: () => BC_CMD_ID_DING_DONG_OPT,
|
|
7334
7344
|
BC_CMD_ID_FILE_INFO_LIST_CLOSE: () => BC_CMD_ID_FILE_INFO_LIST_CLOSE,
|
|
7335
7345
|
BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO: () => BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO,
|
|
7336
7346
|
BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD: () => BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD,
|
|
@@ -7354,6 +7364,9 @@ __export(index_exports, {
|
|
|
7354
7364
|
BC_CMD_ID_GET_BATTERY_INFO_LIST: () => BC_CMD_ID_GET_BATTERY_INFO_LIST,
|
|
7355
7365
|
BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
7356
7366
|
BC_CMD_ID_GET_DAY_RECORDS: () => BC_CMD_ID_GET_DAY_RECORDS,
|
|
7367
|
+
BC_CMD_ID_GET_DING_DONG_CFG: () => BC_CMD_ID_GET_DING_DONG_CFG,
|
|
7368
|
+
BC_CMD_ID_GET_DING_DONG_LIST: () => BC_CMD_ID_GET_DING_DONG_LIST,
|
|
7369
|
+
BC_CMD_ID_GET_DING_DONG_SILENT: () => BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
7357
7370
|
BC_CMD_ID_GET_EMAIL_TASK: () => BC_CMD_ID_GET_EMAIL_TASK,
|
|
7358
7371
|
BC_CMD_ID_GET_FTP_TASK: () => BC_CMD_ID_GET_FTP_TASK,
|
|
7359
7372
|
BC_CMD_ID_GET_HDD_INFO_LIST: () => BC_CMD_ID_GET_HDD_INFO_LIST,
|
|
@@ -7390,9 +7403,12 @@ __export(index_exports, {
|
|
|
7390
7403
|
BC_CMD_ID_PUSH_SERIAL: () => BC_CMD_ID_PUSH_SERIAL,
|
|
7391
7404
|
BC_CMD_ID_PUSH_SLEEP_STATUS: () => BC_CMD_ID_PUSH_SLEEP_STATUS,
|
|
7392
7405
|
BC_CMD_ID_PUSH_VIDEO_INPUT: () => BC_CMD_ID_PUSH_VIDEO_INPUT,
|
|
7406
|
+
BC_CMD_ID_QUICK_REPLY_PLAY: () => BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
7393
7407
|
BC_CMD_ID_SET_AI_ALARM: () => BC_CMD_ID_SET_AI_ALARM,
|
|
7394
7408
|
BC_CMD_ID_SET_AI_CFG: () => BC_CMD_ID_SET_AI_CFG,
|
|
7395
7409
|
BC_CMD_ID_SET_AUDIO_TASK: () => BC_CMD_ID_SET_AUDIO_TASK,
|
|
7410
|
+
BC_CMD_ID_SET_DING_DONG_CFG: () => BC_CMD_ID_SET_DING_DONG_CFG,
|
|
7411
|
+
BC_CMD_ID_SET_DING_DONG_SILENT: () => BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
7396
7412
|
BC_CMD_ID_SET_MOTION_ALARM: () => BC_CMD_ID_SET_MOTION_ALARM,
|
|
7397
7413
|
BC_CMD_ID_SET_PIR_INFO: () => BC_CMD_ID_SET_PIR_INFO,
|
|
7398
7414
|
BC_CMD_ID_SET_WHITE_LED_STATE: () => BC_CMD_ID_SET_WHITE_LED_STATE,
|
|
@@ -7509,6 +7525,7 @@ __export(index_exports, {
|
|
|
7509
7525
|
getGlobalLogger: () => getGlobalLogger,
|
|
7510
7526
|
getH265NalType: () => getH265NalType,
|
|
7511
7527
|
getMjpegContentType: () => getMjpegContentType,
|
|
7528
|
+
getSupportItemForChannel: () => getSupportItemForChannel,
|
|
7512
7529
|
getVideoStream: () => getVideoStream,
|
|
7513
7530
|
getVideoclipClientInfo: () => getVideoclipClientInfo,
|
|
7514
7531
|
getXmlText: () => getXmlText,
|
|
@@ -12833,6 +12850,8 @@ var NativeStreamFanout = class {
|
|
|
12833
12850
|
} finally {
|
|
12834
12851
|
for (const q of this.queues.values()) q.close();
|
|
12835
12852
|
this.queues.clear();
|
|
12853
|
+
this.running = false;
|
|
12854
|
+
this.opts.onEnd?.();
|
|
12836
12855
|
}
|
|
12837
12856
|
})();
|
|
12838
12857
|
}
|
|
@@ -12932,6 +12951,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
12932
12951
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
12933
12952
|
nativeFanout = null;
|
|
12934
12953
|
noClientAutoStopTimer;
|
|
12954
|
+
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
12955
|
+
// When a new client connects while the stream is already running it does not need
|
|
12956
|
+
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
12957
|
+
// from the last IDR in the prebuffer immediately.
|
|
12958
|
+
PREBUFFER_MAX_MS = 3e3;
|
|
12959
|
+
prebuffer = [];
|
|
12935
12960
|
static isAdtsAacFrame(b) {
|
|
12936
12961
|
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
12937
12962
|
}
|
|
@@ -12966,6 +12991,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
12966
12991
|
);
|
|
12967
12992
|
return { sampleRate, channels, configHex };
|
|
12968
12993
|
}
|
|
12994
|
+
/** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
|
|
12995
|
+
isRawFrameKeyframe(frame) {
|
|
12996
|
+
try {
|
|
12997
|
+
if (frame.videoType === "H264") {
|
|
12998
|
+
const nals = _BaichuanRtspServer.splitAnnexBNals(
|
|
12999
|
+
convertToAnnexB(frame.data)
|
|
13000
|
+
);
|
|
13001
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
13002
|
+
}
|
|
13003
|
+
if (frame.videoType === "H265") {
|
|
13004
|
+
const nals = splitAnnexBToNalPayloads2(convertToAnnexB2(frame.data));
|
|
13005
|
+
return nals.some(
|
|
13006
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
13007
|
+
);
|
|
13008
|
+
}
|
|
13009
|
+
} catch {
|
|
13010
|
+
}
|
|
13011
|
+
return false;
|
|
13012
|
+
}
|
|
12969
13013
|
static parseInterleavedChannels(transportHeader) {
|
|
12970
13014
|
const m = transportHeader.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
|
|
12971
13015
|
if (!m) return null;
|
|
@@ -13158,7 +13202,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13158
13202
|
this.logger.warn(
|
|
13159
13203
|
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
13160
13204
|
);
|
|
13161
|
-
this.streamMetadata = { frameRate: 25
|
|
13205
|
+
this.streamMetadata = { frameRate: 25 };
|
|
13162
13206
|
this.setFlowVideoType("H264", "metadata unavailable");
|
|
13163
13207
|
}
|
|
13164
13208
|
this.clientConnectionServer = net2.createServer((socket) => {
|
|
@@ -13190,7 +13234,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13190
13234
|
*/
|
|
13191
13235
|
handleRtspConnection(socket) {
|
|
13192
13236
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
13193
|
-
|
|
13237
|
+
const connectTime = Date.now();
|
|
13238
|
+
this.logger.info(
|
|
13239
|
+
`[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
|
|
13240
|
+
);
|
|
13194
13241
|
let sessionId = "";
|
|
13195
13242
|
let buffer = Buffer.alloc(0);
|
|
13196
13243
|
let clientFfmpeg;
|
|
@@ -13198,6 +13245,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13198
13245
|
let clientUdpSocket = null;
|
|
13199
13246
|
let clientUdpSocketAudio = null;
|
|
13200
13247
|
const cleanup = () => {
|
|
13248
|
+
const sessionDurationMs = Date.now() - connectTime;
|
|
13249
|
+
const res = this.clientResources.get(clientId);
|
|
13250
|
+
const framesSent = res?.framesSent ?? 0;
|
|
13251
|
+
this.logger.info(
|
|
13252
|
+
`[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
|
|
13253
|
+
);
|
|
13201
13254
|
this.removeClient(clientId);
|
|
13202
13255
|
this.authNonces.delete(clientId);
|
|
13203
13256
|
const resources = this.clientResources.get(clientId);
|
|
@@ -13339,7 +13392,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13339
13392
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
13340
13393
|
});
|
|
13341
13394
|
} else if (method === "DESCRIBE") {
|
|
13342
|
-
if (!this.
|
|
13395
|
+
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
13343
13396
|
try {
|
|
13344
13397
|
if (!this.nativeStreamActive) {
|
|
13345
13398
|
await this.startNativeStream();
|
|
@@ -13351,7 +13404,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13351
13404
|
}
|
|
13352
13405
|
const { hasParamSets: hasParamSets2 } = this.flow.getFmtp();
|
|
13353
13406
|
if (!hasParamSets2) {
|
|
13354
|
-
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 :
|
|
13407
|
+
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 3e3;
|
|
13408
|
+
const primingStart = Date.now();
|
|
13409
|
+
this.logger.info(
|
|
13410
|
+
`[rebroadcast] DESCRIBE priming: waiting up to ${primingMs}ms for SPS/PPS client=${clientId} path=${this.path}`
|
|
13411
|
+
);
|
|
13355
13412
|
try {
|
|
13356
13413
|
await Promise.race([
|
|
13357
13414
|
this.firstFramePromise || Promise.resolve(),
|
|
@@ -13359,6 +13416,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13359
13416
|
]);
|
|
13360
13417
|
} catch {
|
|
13361
13418
|
}
|
|
13419
|
+
const primingElapsed = Date.now() - primingStart;
|
|
13420
|
+
const { hasParamSets: hasParamSetsAfter } = this.flow.getFmtp();
|
|
13421
|
+
if (hasParamSetsAfter) {
|
|
13422
|
+
this.logger.info(
|
|
13423
|
+
`[rebroadcast] DESCRIBE priming: SPS/PPS received after ${primingElapsed}ms client=${clientId} path=${this.path}`
|
|
13424
|
+
);
|
|
13425
|
+
} else {
|
|
13426
|
+
this.logger.warn(
|
|
13427
|
+
`[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}`
|
|
13428
|
+
);
|
|
13429
|
+
}
|
|
13362
13430
|
}
|
|
13363
13431
|
}
|
|
13364
13432
|
{
|
|
@@ -13367,11 +13435,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13367
13435
|
this.logger.info(
|
|
13368
13436
|
`[BaichuanRtspServer] DESCRIBE SDP for ${clientId} path=${this.path} codec=${this.flow.sdpCodec} hasParamSets=${hasParamSets2} fmtp=${fmtpPreview}`
|
|
13369
13437
|
);
|
|
13370
|
-
if (!hasParamSets2) {
|
|
13371
|
-
this.rtspDebugLog(
|
|
13372
|
-
`DESCRIBE responding without parameter sets yet (client=${clientId}, path=${this.path}, flow=${this.flow.key})`
|
|
13373
|
-
);
|
|
13374
|
-
}
|
|
13375
13438
|
}
|
|
13376
13439
|
const sdp = this.generateSdp();
|
|
13377
13440
|
sendResponse(
|
|
@@ -13421,7 +13484,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13421
13484
|
seenFirstVideoKeyframe: false,
|
|
13422
13485
|
setupTrack0: false,
|
|
13423
13486
|
setupTrack1: false,
|
|
13424
|
-
isPlaying: false
|
|
13487
|
+
isPlaying: false,
|
|
13488
|
+
connectTime
|
|
13425
13489
|
});
|
|
13426
13490
|
} else {
|
|
13427
13491
|
existing.rtspSocket = socket;
|
|
@@ -13468,8 +13532,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13468
13532
|
if (resources) {
|
|
13469
13533
|
if (isTrack1) resources.setupTrack1 = true;
|
|
13470
13534
|
else resources.setupTrack0 = true;
|
|
13471
|
-
|
|
13472
|
-
|
|
13535
|
+
const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
|
|
13536
|
+
const track = isTrack1 ? "track1(audio)" : "track0(video)";
|
|
13537
|
+
this.logger.info(
|
|
13538
|
+
`[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
|
|
13473
13539
|
);
|
|
13474
13540
|
}
|
|
13475
13541
|
}
|
|
@@ -13494,8 +13560,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13494
13560
|
const resources = this.clientResources.get(clientId);
|
|
13495
13561
|
if (resources) {
|
|
13496
13562
|
resources.isPlaying = true;
|
|
13497
|
-
|
|
13498
|
-
|
|
13563
|
+
const hasAudio = !!resources.setupTrack1;
|
|
13564
|
+
this.logger.info(
|
|
13565
|
+
`[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
|
|
13499
13566
|
);
|
|
13500
13567
|
}
|
|
13501
13568
|
}
|
|
@@ -13504,6 +13571,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13504
13571
|
Range: "npt=0.000-"
|
|
13505
13572
|
});
|
|
13506
13573
|
} else if (method === "TEARDOWN") {
|
|
13574
|
+
this.logger.info(
|
|
13575
|
+
`[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
|
|
13576
|
+
);
|
|
13507
13577
|
cleanup();
|
|
13508
13578
|
sendResponse(200, "OK", {
|
|
13509
13579
|
Session: sessionId
|
|
@@ -13569,10 +13639,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13569
13639
|
sdp += `a=control:track1\r
|
|
13570
13640
|
`;
|
|
13571
13641
|
}
|
|
13572
|
-
sdp += `a=setup:passive\r
|
|
13573
|
-
`;
|
|
13574
|
-
sdp += `a=connection:new\r
|
|
13575
|
-
`;
|
|
13576
13642
|
return sdp;
|
|
13577
13643
|
}
|
|
13578
13644
|
/**
|
|
@@ -13598,7 +13664,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13598
13664
|
this.logger.warn(
|
|
13599
13665
|
`[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
|
|
13600
13666
|
);
|
|
13601
|
-
streamMetadata = { frameRate: 25
|
|
13667
|
+
streamMetadata = { frameRate: 25 };
|
|
13602
13668
|
}
|
|
13603
13669
|
}
|
|
13604
13670
|
const ffmpegFormat = this.flow.ffmpegFormat;
|
|
@@ -13643,6 +13709,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13643
13709
|
return false;
|
|
13644
13710
|
if (channel === audioRtpChannel && !resources2?.setupTrack1)
|
|
13645
13711
|
return false;
|
|
13712
|
+
const buffered = rtspSocket.writableLength;
|
|
13713
|
+
if (buffered > 10 * 1024 * 1024) {
|
|
13714
|
+
this.logger.warn(
|
|
13715
|
+
`[rebroadcast] backpressure: ${Math.round(buffered / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
|
|
13716
|
+
);
|
|
13717
|
+
rtspSocket.destroy();
|
|
13718
|
+
return false;
|
|
13719
|
+
}
|
|
13646
13720
|
try {
|
|
13647
13721
|
return rtspSocket.write(frameRtpOverTcp(channel, msg));
|
|
13648
13722
|
} catch (error) {
|
|
@@ -14072,6 +14146,24 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14072
14146
|
let frameCount = 0;
|
|
14073
14147
|
let lastFrameTime = Date.now();
|
|
14074
14148
|
const targetFrameInterval = streamMetadata && streamMetadata.frameRate > 0 ? 1e3 / streamMetadata.frameRate : 40;
|
|
14149
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
14150
|
+
let lastIdrIdx = -1;
|
|
14151
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
14152
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
14153
|
+
lastIdrIdx = i;
|
|
14154
|
+
break;
|
|
14155
|
+
}
|
|
14156
|
+
}
|
|
14157
|
+
const prebufferFrames = lastIdrIdx >= 0 ? prebufferSnap.slice(lastIdrIdx) : [];
|
|
14158
|
+
if (prebufferFrames.length > 0) {
|
|
14159
|
+
this.logger.info(
|
|
14160
|
+
`[rebroadcast] prebuffer replay client=${clientId} frames=${prebufferFrames.length} starting from IDR`
|
|
14161
|
+
);
|
|
14162
|
+
}
|
|
14163
|
+
const combined = async function* () {
|
|
14164
|
+
for (const entry of prebufferFrames) yield entry.frame;
|
|
14165
|
+
for await (const f of clientGenerator) yield f;
|
|
14166
|
+
};
|
|
14075
14167
|
const feedFrames = async () => {
|
|
14076
14168
|
try {
|
|
14077
14169
|
this.rtspDebugLog(
|
|
@@ -14083,7 +14175,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14083
14175
|
let firstVideoFrameSeenLogged = false;
|
|
14084
14176
|
let h265WaitParamSetsLogged = false;
|
|
14085
14177
|
let h265WaitIrapLogged = false;
|
|
14086
|
-
for await (const frame of
|
|
14178
|
+
for await (const frame of combined()) {
|
|
14087
14179
|
if (!this.connectedClients.has(clientId)) {
|
|
14088
14180
|
this.rtspDebugLog(
|
|
14089
14181
|
`Client ${clientId} disconnected, stopping frame feed`
|
|
@@ -14186,15 +14278,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14186
14278
|
`Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
|
|
14187
14279
|
);
|
|
14188
14280
|
}
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14281
|
+
if (!useDirectRtp) {
|
|
14282
|
+
const now = Date.now();
|
|
14283
|
+
const timeSinceLastFrame = now - lastFrameTime;
|
|
14284
|
+
const waitTime = targetFrameInterval - timeSinceLastFrame;
|
|
14285
|
+
if (waitTime > 0) {
|
|
14286
|
+
await new Promise(
|
|
14287
|
+
(resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
|
|
14288
|
+
);
|
|
14289
|
+
}
|
|
14290
|
+
lastFrameTime = Date.now();
|
|
14196
14291
|
}
|
|
14197
|
-
lastFrameTime = Date.now();
|
|
14198
14292
|
if (useDirectRtp) {
|
|
14199
14293
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
14200
14294
|
const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
|
|
@@ -14267,6 +14361,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14267
14361
|
}
|
|
14268
14362
|
if (!firstVideoWriteLogged) {
|
|
14269
14363
|
firstVideoWriteLogged = true;
|
|
14364
|
+
const clientConnectTime = resources?.connectTime ?? Date.now();
|
|
14365
|
+
const ttffMs = Date.now() - clientConnectTime;
|
|
14366
|
+
this.logger.info(
|
|
14367
|
+
`[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
|
|
14368
|
+
);
|
|
14270
14369
|
if (rtspDebug) {
|
|
14271
14370
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
14272
14371
|
rtspDebugLog(
|
|
@@ -14274,6 +14373,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14274
14373
|
);
|
|
14275
14374
|
}
|
|
14276
14375
|
}
|
|
14376
|
+
if (resources) {
|
|
14377
|
+
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
14378
|
+
}
|
|
14277
14379
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
14278
14380
|
} else {
|
|
14279
14381
|
try {
|
|
@@ -14358,8 +14460,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14358
14460
|
this.firstAudioPromise = new Promise((resolve) => {
|
|
14359
14461
|
this.firstAudioResolve = resolve;
|
|
14360
14462
|
});
|
|
14361
|
-
this.
|
|
14362
|
-
`
|
|
14463
|
+
this.logger.info(
|
|
14464
|
+
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
14363
14465
|
);
|
|
14364
14466
|
await this.flow.startKeepAlive(this.api);
|
|
14365
14467
|
this.nativeFanout = new NativeStreamFanout({
|
|
@@ -14397,11 +14499,41 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14397
14499
|
if (hasParamSets2) {
|
|
14398
14500
|
this.markFirstFrameReceived();
|
|
14399
14501
|
}
|
|
14502
|
+
const isKeyframe = this.isRawFrameKeyframe(frame);
|
|
14503
|
+
this.prebuffer.push({
|
|
14504
|
+
frame: { ...frame, data: Buffer.from(frame.data) },
|
|
14505
|
+
time: Date.now(),
|
|
14506
|
+
isKeyframe
|
|
14507
|
+
});
|
|
14508
|
+
const cutoff = Date.now() - this.PREBUFFER_MAX_MS;
|
|
14509
|
+
let trimIdx = 0;
|
|
14510
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
14511
|
+
trimIdx++;
|
|
14512
|
+
}
|
|
14513
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
14400
14514
|
},
|
|
14401
14515
|
onError: (error) => {
|
|
14402
14516
|
this.logger.warn(
|
|
14403
14517
|
`[BaichuanRtspServer] Shared native stream error: ${error}`
|
|
14404
14518
|
);
|
|
14519
|
+
},
|
|
14520
|
+
onEnd: () => {
|
|
14521
|
+
if (!this.nativeStreamActive) return;
|
|
14522
|
+
this.nativeStreamActive = false;
|
|
14523
|
+
this.firstFrameReceived = false;
|
|
14524
|
+
this.firstFramePromise = null;
|
|
14525
|
+
this.firstFrameResolve = null;
|
|
14526
|
+
this.nativeFanout = null;
|
|
14527
|
+
this.prebuffer = [];
|
|
14528
|
+
this.logger.info(
|
|
14529
|
+
`[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
14530
|
+
);
|
|
14531
|
+
if (this.connectedClients.size > 0) {
|
|
14532
|
+
this.logger.info(
|
|
14533
|
+
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
14534
|
+
);
|
|
14535
|
+
setImmediate(() => void this.startNativeStream());
|
|
14536
|
+
}
|
|
14405
14537
|
}
|
|
14406
14538
|
});
|
|
14407
14539
|
this.nativeFanout.start();
|
|
@@ -14440,7 +14572,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14440
14572
|
if (!this.nativeStreamActive) {
|
|
14441
14573
|
return;
|
|
14442
14574
|
}
|
|
14443
|
-
this.
|
|
14575
|
+
this.logger.info(
|
|
14576
|
+
`[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
14577
|
+
);
|
|
14444
14578
|
this.flow.stopKeepAlive();
|
|
14445
14579
|
this.clearNoClientAutoStopTimer();
|
|
14446
14580
|
this.nativeStreamActive = false;
|
|
@@ -14459,6 +14593,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14459
14593
|
this.nativeFanout = null;
|
|
14460
14594
|
await fanout.stop();
|
|
14461
14595
|
}
|
|
14596
|
+
this.prebuffer = [];
|
|
14462
14597
|
if (this.tempStreamGenerator) {
|
|
14463
14598
|
try {
|
|
14464
14599
|
await this.tempStreamGenerator.return(void 0);
|
|
@@ -14474,9 +14609,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14474
14609
|
if (this.connectedClients.has(clientId)) {
|
|
14475
14610
|
this.connectedClients.delete(clientId);
|
|
14476
14611
|
this.emit("clientDisconnected", clientId);
|
|
14477
|
-
this.logger.info(
|
|
14478
|
-
`[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
|
|
14479
|
-
);
|
|
14480
14612
|
if (this.connectedClients.size === 0) {
|
|
14481
14613
|
void this.stopNativeStream();
|
|
14482
14614
|
}
|
|
@@ -14951,10 +15083,12 @@ function parseSupportXml(xml) {
|
|
|
14951
15083
|
}
|
|
14952
15084
|
function getSupportItemForChannel(support, channel) {
|
|
14953
15085
|
if (!support?.items?.length) return void 0;
|
|
14954
|
-
const
|
|
15086
|
+
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
15087
|
+
if (!candidates.length) return void 0;
|
|
15088
|
+
const score = (item) => {
|
|
14955
15089
|
const anyItem = item;
|
|
14956
|
-
let
|
|
14957
|
-
if (anyItem.name == null)
|
|
15090
|
+
let result = 0;
|
|
15091
|
+
if (anyItem.name == null) result += 100;
|
|
14958
15092
|
const capabilityKeys = [
|
|
14959
15093
|
"ptzType",
|
|
14960
15094
|
"ptzControl",
|
|
@@ -14966,20 +15100,17 @@ function getSupportItemForChannel(support, channel) {
|
|
|
14966
15100
|
"motion",
|
|
14967
15101
|
"encCtrl",
|
|
14968
15102
|
"newIspCfg",
|
|
14969
|
-
"remoteAbility"
|
|
15103
|
+
"remoteAbility",
|
|
15104
|
+
"aitype",
|
|
15105
|
+
"videoClip",
|
|
15106
|
+
"snap"
|
|
14970
15107
|
];
|
|
14971
15108
|
for (const k of capabilityKeys) {
|
|
14972
|
-
if (anyItem[k] !== void 0)
|
|
15109
|
+
if (anyItem[k] !== void 0) result += 3;
|
|
14973
15110
|
}
|
|
14974
|
-
|
|
14975
|
-
return score;
|
|
14976
|
-
};
|
|
14977
|
-
const pickBest = (chnId) => {
|
|
14978
|
-
const candidates = support.items.filter((i) => i.chnID === chnId);
|
|
14979
|
-
if (!candidates.length) return void 0;
|
|
14980
|
-
return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
|
|
15111
|
+
return result;
|
|
14981
15112
|
};
|
|
14982
|
-
return
|
|
15113
|
+
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
14983
15114
|
}
|
|
14984
15115
|
function computeDeviceCapabilities(params) {
|
|
14985
15116
|
const { channel } = params;
|
|
@@ -15011,6 +15142,7 @@ function computeDeviceCapabilities(params) {
|
|
|
15011
15142
|
flat,
|
|
15012
15143
|
/white\s*led|whiteLed|flood\s*light|floodlight/i
|
|
15013
15144
|
);
|
|
15145
|
+
const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
|
|
15014
15146
|
const hasSirenFromAbilities = abilitiesHasAny(
|
|
15015
15147
|
flat,
|
|
15016
15148
|
/audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
|
|
@@ -15023,6 +15155,9 @@ function computeDeviceCapabilities(params) {
|
|
|
15023
15155
|
const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
|
|
15024
15156
|
const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
|
|
15025
15157
|
const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
|
|
15158
|
+
const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
|
|
15159
|
+
const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
|
|
15160
|
+
const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
|
|
15026
15161
|
const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
15027
15162
|
const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
15028
15163
|
const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
|
|
@@ -15038,14 +15173,15 @@ function computeDeviceCapabilities(params) {
|
|
|
15038
15173
|
hasZoom: finalHasZoom,
|
|
15039
15174
|
hasPresets: finalHasPresets,
|
|
15040
15175
|
hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
|
|
15041
|
-
hasBattery
|
|
15176
|
+
hasBattery,
|
|
15042
15177
|
hasIntercom: hasIntercomFromSupport,
|
|
15043
|
-
hasSiren: hasSirenFromAbilities,
|
|
15178
|
+
hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
|
|
15044
15179
|
// lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
|
|
15045
15180
|
hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
|
|
15046
15181
|
hasPir: hasPirFromAbilities || hasPirFromSupport,
|
|
15047
|
-
isDoorbell
|
|
15048
|
-
hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
|
|
15182
|
+
isDoorbell,
|
|
15183
|
+
hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
|
|
15184
|
+
hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
|
|
15049
15185
|
};
|
|
15050
15186
|
if (ptzMode !== void 0) result.ptzMode = ptzMode;
|
|
15051
15187
|
return result;
|
|
@@ -16710,6 +16846,162 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
|
|
|
16710
16846
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
16711
16847
|
init_recordingFileName();
|
|
16712
16848
|
|
|
16849
|
+
// src/reolink/baichuan/utils/chime.ts
|
|
16850
|
+
init_xml();
|
|
16851
|
+
var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16852
|
+
<body>
|
|
16853
|
+
<dingdongDeviceOpt version="1.1">
|
|
16854
|
+
<id>${chimeId}</id>
|
|
16855
|
+
<opt>getParam</opt>
|
|
16856
|
+
</dingdongDeviceOpt>
|
|
16857
|
+
</body>`;
|
|
16858
|
+
var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16859
|
+
<body>
|
|
16860
|
+
<dingdongDeviceOpt version="1.1">
|
|
16861
|
+
<opt>setParam</opt>
|
|
16862
|
+
<id>${chimeId}</id>
|
|
16863
|
+
${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
|
|
16864
|
+
${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
|
|
16865
|
+
${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
|
|
16866
|
+
</dingdongDeviceOpt>
|
|
16867
|
+
</body>`;
|
|
16868
|
+
var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16869
|
+
<body>
|
|
16870
|
+
<dingdongDeviceOpt version="1.1">
|
|
16871
|
+
<id>${chimeId}</id>
|
|
16872
|
+
<opt>ringWithMusic</opt>
|
|
16873
|
+
<musicId>${musicId}</musicId>
|
|
16874
|
+
</dingdongDeviceOpt>
|
|
16875
|
+
</body>`;
|
|
16876
|
+
var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16877
|
+
<body>
|
|
16878
|
+
<dingdongCfg version="1.1">
|
|
16879
|
+
<deviceCfg>
|
|
16880
|
+
<id>${chimeId}</id>
|
|
16881
|
+
<alarminCfg>
|
|
16882
|
+
<valid>${state}</valid>
|
|
16883
|
+
<musicId>${musicId}</musicId>
|
|
16884
|
+
<type>${eventType}</type>
|
|
16885
|
+
</alarminCfg>
|
|
16886
|
+
</deviceCfg>
|
|
16887
|
+
</dingdongCfg>
|
|
16888
|
+
</body>`;
|
|
16889
|
+
var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16890
|
+
<body>
|
|
16891
|
+
<dingdongCtrl version="1.1">
|
|
16892
|
+
<opt>machineStateGet</opt>
|
|
16893
|
+
</dingdongCtrl>
|
|
16894
|
+
</body>`;
|
|
16895
|
+
var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16896
|
+
<body>
|
|
16897
|
+
<dingdongCtrl version="1.1">
|
|
16898
|
+
<opt>machineStateSet</opt>
|
|
16899
|
+
<type>${chimeType}</type>
|
|
16900
|
+
<bopen>${enabled}</bopen>
|
|
16901
|
+
<bsave>1</bsave>
|
|
16902
|
+
<time>${time}</time>
|
|
16903
|
+
</dingdongCtrl>
|
|
16904
|
+
</body>`;
|
|
16905
|
+
var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16906
|
+
<body>
|
|
16907
|
+
<audioFileInfo version="1.1">
|
|
16908
|
+
<channelId>${channel}</channelId>
|
|
16909
|
+
<id>${fileId}</id>
|
|
16910
|
+
<timeout>0</timeout>
|
|
16911
|
+
</audioFileInfo>
|
|
16912
|
+
</body>`;
|
|
16913
|
+
var parseDingDongListFromXml = (xml) => {
|
|
16914
|
+
const devices = [];
|
|
16915
|
+
const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
|
|
16916
|
+
for (const block of blocks) {
|
|
16917
|
+
const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
|
|
16918
|
+
const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
|
|
16919
|
+
const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
|
|
16920
|
+
if (idText === void 0) continue;
|
|
16921
|
+
const id = Number(idText);
|
|
16922
|
+
if (!Number.isFinite(id)) continue;
|
|
16923
|
+
devices.push({
|
|
16924
|
+
id,
|
|
16925
|
+
name,
|
|
16926
|
+
netState: netStateText !== void 0 ? Number(netStateText) : 0
|
|
16927
|
+
});
|
|
16928
|
+
}
|
|
16929
|
+
return devices;
|
|
16930
|
+
};
|
|
16931
|
+
var parseDingDongParamsFromXml = (xml) => {
|
|
16932
|
+
const name = getXmlText(xml, "name");
|
|
16933
|
+
const volLevelText = getXmlText(xml, "volLevel");
|
|
16934
|
+
const ledStateText = getXmlText(xml, "ledState");
|
|
16935
|
+
const result = {};
|
|
16936
|
+
if (name !== void 0) result.name = name;
|
|
16937
|
+
if (volLevelText !== void 0) {
|
|
16938
|
+
const n = Number(volLevelText);
|
|
16939
|
+
if (Number.isFinite(n)) result.volLevel = n;
|
|
16940
|
+
}
|
|
16941
|
+
if (ledStateText !== void 0) {
|
|
16942
|
+
const n = Number(ledStateText);
|
|
16943
|
+
if (Number.isFinite(n)) result.ledState = n;
|
|
16944
|
+
}
|
|
16945
|
+
return result;
|
|
16946
|
+
};
|
|
16947
|
+
var parseDingDongCfgFromXml = (xml) => {
|
|
16948
|
+
const configs = [];
|
|
16949
|
+
const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
|
|
16950
|
+
for (const deviceBlock of deviceBlocks) {
|
|
16951
|
+
const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
|
|
16952
|
+
if (idText === void 0) continue;
|
|
16953
|
+
const id = Number(idText);
|
|
16954
|
+
if (!Number.isFinite(id)) continue;
|
|
16955
|
+
const typeMap = {};
|
|
16956
|
+
const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
|
|
16957
|
+
for (const alarmBlock of alarmBlocks) {
|
|
16958
|
+
const type = getXmlText(alarmBlock, "type");
|
|
16959
|
+
if (!type) continue;
|
|
16960
|
+
const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
|
|
16961
|
+
const musicIdText = getXmlText(alarmBlock, "musicId");
|
|
16962
|
+
typeMap[type] = {
|
|
16963
|
+
valid: validText !== void 0 ? Number(validText) : 0,
|
|
16964
|
+
musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
|
|
16965
|
+
};
|
|
16966
|
+
}
|
|
16967
|
+
configs.push({ id, type: typeMap });
|
|
16968
|
+
}
|
|
16969
|
+
return configs;
|
|
16970
|
+
};
|
|
16971
|
+
var parseHardwiredChimeFromXml = (xml) => {
|
|
16972
|
+
const type = getXmlText(xml, "type") ?? "";
|
|
16973
|
+
const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
|
|
16974
|
+
const timeText = getXmlText(xml, "time");
|
|
16975
|
+
return {
|
|
16976
|
+
type,
|
|
16977
|
+
enabled: bopenText === "1",
|
|
16978
|
+
time: timeText !== void 0 ? Number(timeText) : 0
|
|
16979
|
+
};
|
|
16980
|
+
};
|
|
16981
|
+
var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16982
|
+
<body>
|
|
16983
|
+
<dingdongSilentMode version="1.1">
|
|
16984
|
+
<id>${chimeId}</id>
|
|
16985
|
+
</dingdongSilentMode>
|
|
16986
|
+
</body>`;
|
|
16987
|
+
var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
16988
|
+
<body>
|
|
16989
|
+
<dingdongSilentMode version="1.1">
|
|
16990
|
+
<id>${chimeId}</id>
|
|
16991
|
+
<time>${time}</time>
|
|
16992
|
+
<type>63</type>
|
|
16993
|
+
</dingdongSilentMode>
|
|
16994
|
+
</body>`;
|
|
16995
|
+
var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
|
|
16996
|
+
const timeText = getXmlText(xml, "time");
|
|
16997
|
+
const time = timeText !== void 0 ? Number(timeText) : 0;
|
|
16998
|
+
return {
|
|
16999
|
+
id: chimeId,
|
|
17000
|
+
time,
|
|
17001
|
+
active: time === 0
|
|
17002
|
+
};
|
|
17003
|
+
};
|
|
17004
|
+
|
|
16713
17005
|
// src/reolink/baichuan/utils/eventsGetEvents.ts
|
|
16714
17006
|
init_xml();
|
|
16715
17007
|
var parseAiTypeToken = (aiTypeRaw) => {
|
|
@@ -17022,6 +17314,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17022
17314
|
host;
|
|
17023
17315
|
username;
|
|
17024
17316
|
password;
|
|
17317
|
+
/**
|
|
17318
|
+
* Set to `true` after `close()` is called.
|
|
17319
|
+
* Once closed, the API instance should not be reused.
|
|
17320
|
+
*/
|
|
17321
|
+
_closed = false;
|
|
17025
17322
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
17026
17323
|
// SOCKET POOL - Tag-based socket management
|
|
17027
17324
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -17051,10 +17348,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17051
17348
|
get client() {
|
|
17052
17349
|
const entry = this.socketPool.get("general");
|
|
17053
17350
|
if (!entry) {
|
|
17351
|
+
if (this._closed) {
|
|
17352
|
+
throw new Error(
|
|
17353
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
17354
|
+
);
|
|
17355
|
+
}
|
|
17054
17356
|
throw new Error("[ReolinkBaichuanApi] General socket not initialized");
|
|
17055
17357
|
}
|
|
17056
17358
|
return entry.client;
|
|
17057
17359
|
}
|
|
17360
|
+
/**
|
|
17361
|
+
* `true` after `close()` has been called. A closed API should not be reused;
|
|
17362
|
+
* the consumer should create a new instance.
|
|
17363
|
+
*/
|
|
17364
|
+
get isClosed() {
|
|
17365
|
+
return this._closed;
|
|
17366
|
+
}
|
|
17367
|
+
/**
|
|
17368
|
+
* `true` when the API is usable: not closed, general socket exists, socket
|
|
17369
|
+
* is connected and the client is logged in.
|
|
17370
|
+
*
|
|
17371
|
+
* This is the recommended way for consumers to check whether the API is
|
|
17372
|
+
* still valid before issuing commands, instead of directly accessing
|
|
17373
|
+
* `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
|
|
17374
|
+
* if the socket pool was already destroyed).
|
|
17375
|
+
*/
|
|
17376
|
+
get isReady() {
|
|
17377
|
+
if (this._closed) return false;
|
|
17378
|
+
const entry = this.socketPool.get("general");
|
|
17379
|
+
if (!entry) return false;
|
|
17380
|
+
try {
|
|
17381
|
+
return entry.client.isSocketConnected() && entry.client.loggedIn;
|
|
17382
|
+
} catch {
|
|
17383
|
+
return false;
|
|
17384
|
+
}
|
|
17385
|
+
}
|
|
17386
|
+
/** Promise tracking an in-flight reconnection from `ensureConnected()`. */
|
|
17387
|
+
_ensureConnectedPromise;
|
|
17388
|
+
/**
|
|
17389
|
+
* Ensure the "general" socket is connected and logged in.
|
|
17390
|
+
* If the socket is disconnected or the pool entry was destroyed, a new
|
|
17391
|
+
* general socket is created, logged in, and all event/push/guard listeners
|
|
17392
|
+
* are re-attached automatically.
|
|
17393
|
+
*
|
|
17394
|
+
* This is a **no-op** when the API is already {@link isReady}.
|
|
17395
|
+
*
|
|
17396
|
+
* @throws If `close()` was called — the API is permanently closed and a new
|
|
17397
|
+
* instance must be created.
|
|
17398
|
+
*/
|
|
17399
|
+
async ensureConnected() {
|
|
17400
|
+
if (this._closed) {
|
|
17401
|
+
throw new Error(
|
|
17402
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
17403
|
+
);
|
|
17404
|
+
}
|
|
17405
|
+
if (this.isReady) return;
|
|
17406
|
+
if (this._ensureConnectedPromise) {
|
|
17407
|
+
return this._ensureConnectedPromise;
|
|
17408
|
+
}
|
|
17409
|
+
this._ensureConnectedPromise = this.reconnectGeneralSocket();
|
|
17410
|
+
try {
|
|
17411
|
+
await this._ensureConnectedPromise;
|
|
17412
|
+
} finally {
|
|
17413
|
+
this._ensureConnectedPromise = void 0;
|
|
17414
|
+
}
|
|
17415
|
+
}
|
|
17416
|
+
/**
|
|
17417
|
+
* Internal: destroy the current general socket (if any), create a new one,
|
|
17418
|
+
* login, and re-attach all listeners.
|
|
17419
|
+
*/
|
|
17420
|
+
async reconnectGeneralSocket() {
|
|
17421
|
+
const oldEntry = this.socketPool.get("general");
|
|
17422
|
+
if (oldEntry) {
|
|
17423
|
+
oldEntry.client.removeAllListeners();
|
|
17424
|
+
if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
|
|
17425
|
+
if (oldEntry.generalPermitRelease) {
|
|
17426
|
+
try {
|
|
17427
|
+
oldEntry.generalPermitRelease();
|
|
17428
|
+
} catch {
|
|
17429
|
+
}
|
|
17430
|
+
}
|
|
17431
|
+
this.socketPool.delete("general");
|
|
17432
|
+
try {
|
|
17433
|
+
await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
|
|
17434
|
+
} catch {
|
|
17435
|
+
}
|
|
17436
|
+
}
|
|
17437
|
+
const newClient = new BaichuanClient(this.clientOptions);
|
|
17438
|
+
this.socketPool.set("general", {
|
|
17439
|
+
client: newClient,
|
|
17440
|
+
refCount: 1,
|
|
17441
|
+
// general socket is always "in use"
|
|
17442
|
+
createdAt: Date.now(),
|
|
17443
|
+
lastUsedAt: Date.now(),
|
|
17444
|
+
idleCloseTimer: void 0,
|
|
17445
|
+
generalPermitRelease: void 0
|
|
17446
|
+
});
|
|
17447
|
+
this.setupGeneralClientListeners();
|
|
17448
|
+
await this.client.login();
|
|
17449
|
+
this.logger.log?.(
|
|
17450
|
+
"[ReolinkBaichuanApi] General socket reconnected successfully"
|
|
17451
|
+
);
|
|
17452
|
+
if (this.simpleEventListeners.size > 0) {
|
|
17453
|
+
this.simpleEventSubscribed = false;
|
|
17454
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
17455
|
+
this.simpleEventWatchdogLastRecoveryAt = 0;
|
|
17456
|
+
try {
|
|
17457
|
+
await this.ensureSimpleEventSubscribed();
|
|
17458
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
17459
|
+
this.logger.log?.(
|
|
17460
|
+
`[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
|
|
17461
|
+
);
|
|
17462
|
+
} catch (e) {
|
|
17463
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
17464
|
+
this.logger,
|
|
17465
|
+
`[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
|
|
17466
|
+
formatErrorForLog(e)
|
|
17467
|
+
);
|
|
17468
|
+
}
|
|
17469
|
+
}
|
|
17470
|
+
}
|
|
17471
|
+
/**
|
|
17472
|
+
* Attach event, push, channelInfo, and guard listeners to the current
|
|
17473
|
+
* "general" client. Called from the constructor and from
|
|
17474
|
+
* {@link reconnectGeneralSocket}.
|
|
17475
|
+
*/
|
|
17476
|
+
setupGeneralClientListeners() {
|
|
17477
|
+
const client = this.client;
|
|
17478
|
+
client.on("event", (event) => {
|
|
17479
|
+
const mapped = mapToSimpleEvent(event);
|
|
17480
|
+
if (!mapped) return;
|
|
17481
|
+
this.dispatchSimpleEvent(mapped);
|
|
17482
|
+
});
|
|
17483
|
+
client.on("channelInfo", (xml) => {
|
|
17484
|
+
try {
|
|
17485
|
+
this.parseAndStoreChannelInfo(xml);
|
|
17486
|
+
} catch (e) {
|
|
17487
|
+
this.logger.warn?.(
|
|
17488
|
+
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
17489
|
+
formatErrorForLog(e)
|
|
17490
|
+
);
|
|
17491
|
+
}
|
|
17492
|
+
});
|
|
17493
|
+
client.on("push", (frame) => {
|
|
17494
|
+
const cmdId = frame.header.cmdId;
|
|
17495
|
+
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) {
|
|
17496
|
+
return;
|
|
17497
|
+
}
|
|
17498
|
+
try {
|
|
17499
|
+
if (frame.body.length === 0) return;
|
|
17500
|
+
const xml = client.tryDecryptXml(
|
|
17501
|
+
frame.body,
|
|
17502
|
+
frame.header.channelId,
|
|
17503
|
+
client.enc
|
|
17504
|
+
);
|
|
17505
|
+
if (!xml || !xml.startsWith("<?xml")) return;
|
|
17506
|
+
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
17507
|
+
} catch (e) {
|
|
17508
|
+
this.logger.debug?.(
|
|
17509
|
+
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
17510
|
+
formatErrorForLog(e)
|
|
17511
|
+
);
|
|
17512
|
+
}
|
|
17513
|
+
});
|
|
17514
|
+
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
17515
|
+
client.on("close", () => {
|
|
17516
|
+
try {
|
|
17517
|
+
void this.maybeRebootOnDisconnectStorm();
|
|
17518
|
+
} catch {
|
|
17519
|
+
}
|
|
17520
|
+
});
|
|
17521
|
+
}
|
|
17522
|
+
if (this.rebootAfterConsecutiveEconnreset > 0) {
|
|
17523
|
+
client.on("close", () => {
|
|
17524
|
+
try {
|
|
17525
|
+
void this.maybeRebootOnEconnresetStorm();
|
|
17526
|
+
} catch {
|
|
17527
|
+
}
|
|
17528
|
+
});
|
|
17529
|
+
}
|
|
17530
|
+
if (!this.sessionGuardIntervalTimer) {
|
|
17531
|
+
client.once("push", () => {
|
|
17532
|
+
void this.logActiveSessionsOnStartup();
|
|
17533
|
+
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
17534
|
+
void this.maybeRebootOnTooManySessions();
|
|
17535
|
+
}, 6e4);
|
|
17536
|
+
});
|
|
17537
|
+
}
|
|
17538
|
+
}
|
|
17058
17539
|
/**
|
|
17059
17540
|
* Cached camera UID. May be initially undefined if not provided in the constructor.
|
|
17060
17541
|
* Will be lazily populated on demand when needed (e.g. for recordings).
|
|
@@ -17995,42 +18476,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17995
18476
|
logger: this.logger,
|
|
17996
18477
|
debugConfig: generalClient.getDebugConfig?.()
|
|
17997
18478
|
});
|
|
17998
|
-
this.client.on("event", (event) => {
|
|
17999
|
-
const mapped = mapToSimpleEvent(event);
|
|
18000
|
-
if (!mapped) return;
|
|
18001
|
-
this.dispatchSimpleEvent(mapped);
|
|
18002
|
-
});
|
|
18003
|
-
this.client.on("channelInfo", (xml) => {
|
|
18004
|
-
try {
|
|
18005
|
-
this.parseAndStoreChannelInfo(xml);
|
|
18006
|
-
} catch (e) {
|
|
18007
|
-
this.logger.warn?.(
|
|
18008
|
-
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
18009
|
-
formatErrorForLog(e)
|
|
18010
|
-
);
|
|
18011
|
-
}
|
|
18012
|
-
});
|
|
18013
|
-
this.client.on("push", (frame) => {
|
|
18014
|
-
const cmdId = frame.header.cmdId;
|
|
18015
|
-
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) {
|
|
18016
|
-
return;
|
|
18017
|
-
}
|
|
18018
|
-
try {
|
|
18019
|
-
if (frame.body.length === 0) return;
|
|
18020
|
-
const xml = this.client.tryDecryptXml(
|
|
18021
|
-
frame.body,
|
|
18022
|
-
frame.header.channelId,
|
|
18023
|
-
this.client.enc
|
|
18024
|
-
);
|
|
18025
|
-
if (!xml || !xml.startsWith("<?xml")) return;
|
|
18026
|
-
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
18027
|
-
} catch (e) {
|
|
18028
|
-
this.logger.debug?.(
|
|
18029
|
-
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
18030
|
-
formatErrorForLog(e)
|
|
18031
|
-
);
|
|
18032
|
-
}
|
|
18033
|
-
});
|
|
18034
18479
|
const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
|
|
18035
18480
|
if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
|
|
18036
18481
|
this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
|
|
@@ -18039,32 +18484,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18039
18484
|
if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
|
|
18040
18485
|
this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
|
|
18041
18486
|
}
|
|
18042
|
-
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
18043
|
-
this.client.on("close", () => {
|
|
18044
|
-
try {
|
|
18045
|
-
void this.maybeRebootOnDisconnectStorm();
|
|
18046
|
-
} catch {
|
|
18047
|
-
}
|
|
18048
|
-
});
|
|
18049
|
-
}
|
|
18050
18487
|
const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
|
|
18051
18488
|
if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
|
|
18052
18489
|
this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
|
|
18053
18490
|
}
|
|
18054
|
-
|
|
18055
|
-
this.client.on("close", () => {
|
|
18056
|
-
try {
|
|
18057
|
-
void this.maybeRebootOnEconnresetStorm();
|
|
18058
|
-
} catch {
|
|
18059
|
-
}
|
|
18060
|
-
});
|
|
18061
|
-
}
|
|
18062
|
-
this.client.once("push", () => {
|
|
18063
|
-
void this.logActiveSessionsOnStartup();
|
|
18064
|
-
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
18065
|
-
void this.maybeRebootOnTooManySessions();
|
|
18066
|
-
}, 6e4);
|
|
18067
|
-
});
|
|
18491
|
+
this.setupGeneralClientListeners();
|
|
18068
18492
|
}
|
|
18069
18493
|
/**
|
|
18070
18494
|
* CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
|
|
@@ -18895,6 +19319,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18895
19319
|
);
|
|
18896
19320
|
}
|
|
18897
19321
|
async close(options) {
|
|
19322
|
+
if (this._closed) return;
|
|
19323
|
+
this._closed = true;
|
|
18898
19324
|
if (this.sessionGuardIntervalTimer) {
|
|
18899
19325
|
clearInterval(this.sessionGuardIntervalTimer);
|
|
18900
19326
|
this.sessionGuardIntervalTimer = void 0;
|
|
@@ -18957,7 +19383,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18957
19383
|
}
|
|
18958
19384
|
async handleSendXml400(params, frame, retry) {
|
|
18959
19385
|
const emptyBody = frame.body.length === 0;
|
|
18960
|
-
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes:
|
|
19386
|
+
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
|
|
18961
19387
|
if (this.isSendXmlFailFast400(params, frame.body.length)) {
|
|
18962
19388
|
throw new Error(emptyBody400Msg);
|
|
18963
19389
|
}
|
|
@@ -19473,11 +19899,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19473
19899
|
* Minimal per-channel inventory for NVR-connected devices.
|
|
19474
19900
|
*
|
|
19475
19901
|
* Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
|
|
19902
|
+
*
|
|
19903
|
+
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
19904
|
+
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
19905
|
+
* no dependency on async push messages. Recommended for first-call discovery.
|
|
19906
|
+
* - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
|
|
19907
|
+
* info after login + event subscription. This push is *asynchronous*: if it has not
|
|
19908
|
+
* arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
|
|
19909
|
+
* with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
|
|
19476
19910
|
*/
|
|
19477
19911
|
async getNvrChannelsSummary(options) {
|
|
19478
|
-
const source = options?.source ?? "
|
|
19479
|
-
|
|
19480
|
-
const
|
|
19912
|
+
const source = options?.source ?? "cgi";
|
|
19913
|
+
let channels;
|
|
19914
|
+
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
19915
|
+
if (options?.channels?.length) {
|
|
19916
|
+
channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
19917
|
+
} else if (source === "cgi") {
|
|
19918
|
+
try {
|
|
19919
|
+
const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
|
|
19920
|
+
const status = channelsResponse?.[0]?.value?.status ?? [];
|
|
19921
|
+
for (const s of status) {
|
|
19922
|
+
const ch = Number(s?.channel);
|
|
19923
|
+
if (!Number.isFinite(ch)) continue;
|
|
19924
|
+
cgiStatusByChannel.set(ch, {
|
|
19925
|
+
...s.name != null ? { name: s.name } : {},
|
|
19926
|
+
...s.uid != null ? { uid: s.uid } : {},
|
|
19927
|
+
sleeping: s.sleep === 1
|
|
19928
|
+
});
|
|
19929
|
+
}
|
|
19930
|
+
channels = cgiChannels;
|
|
19931
|
+
this.logger.debug?.(
|
|
19932
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
19933
|
+
);
|
|
19934
|
+
} catch (e) {
|
|
19935
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19936
|
+
this.logger.warn?.(
|
|
19937
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
|
|
19938
|
+
);
|
|
19939
|
+
channels = [];
|
|
19940
|
+
}
|
|
19941
|
+
} else {
|
|
19942
|
+
const pushInfo2 = this.getChannelInfoFromPushCache();
|
|
19943
|
+
channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
19944
|
+
}
|
|
19945
|
+
channels = channels.sort((a, b) => a - b);
|
|
19481
19946
|
const support = await this.getSupportInfo().catch(() => {
|
|
19482
19947
|
this.logger.error?.(
|
|
19483
19948
|
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
@@ -19507,7 +19972,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19507
19972
|
);
|
|
19508
19973
|
}
|
|
19509
19974
|
}
|
|
19510
|
-
const cacheKey =
|
|
19975
|
+
const cacheKey = `${source}:${channels.join(",")}`;
|
|
19511
19976
|
const cached = this.nvrChannelsSummaryCache.get(cacheKey);
|
|
19512
19977
|
if (cached) {
|
|
19513
19978
|
return {
|
|
@@ -19528,8 +19993,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19528
19993
|
} catch {
|
|
19529
19994
|
}
|
|
19530
19995
|
}
|
|
19996
|
+
const pushInfo = this.getChannelInfoFromPushCache();
|
|
19531
19997
|
const devices = channels.map((channel) => {
|
|
19532
|
-
const
|
|
19998
|
+
const pushCached = pushInfo.get(channel);
|
|
19999
|
+
const cgiStatus = cgiStatusByChannel.get(channel);
|
|
19533
20000
|
const info = infoPerChannel.get(channel);
|
|
19534
20001
|
const networkInfo = networkInfoPerChannel.get(channel);
|
|
19535
20002
|
const isBattery = isBatteryByChannel.get(channel) ?? false;
|
|
@@ -19537,6 +20004,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19537
20004
|
const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
|
|
19538
20005
|
const normalizedModel = model ? model.trim() : void 0;
|
|
19539
20006
|
const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
20007
|
+
const name = pushCached?.name || cgiStatus?.name || "";
|
|
20008
|
+
const uid = pushCached?.uid || cgiStatus?.uid || "";
|
|
20009
|
+
const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
|
|
19540
20010
|
return {
|
|
19541
20011
|
channel,
|
|
19542
20012
|
isBattery,
|
|
@@ -19546,19 +20016,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19546
20016
|
...networkInfo?.ip ? { ip: networkInfo.ip } : {},
|
|
19547
20017
|
...networkInfo?.mac ? { mac: networkInfo.mac } : {},
|
|
19548
20018
|
...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
|
|
19549
|
-
...
|
|
19550
|
-
...
|
|
19551
|
-
...
|
|
19552
|
-
...typeof
|
|
19553
|
-
...
|
|
19554
|
-
...
|
|
19555
|
-
...
|
|
19556
|
-
...typeof
|
|
19557
|
-
...typeof
|
|
19558
|
-
...typeof
|
|
19559
|
-
...typeof
|
|
19560
|
-
...
|
|
19561
|
-
...typeof
|
|
20019
|
+
...name ? { name } : {},
|
|
20020
|
+
...uid ? { uid } : {},
|
|
20021
|
+
...pushCached?.state ? { state: pushCached.state } : {},
|
|
20022
|
+
...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
|
|
20023
|
+
...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
|
|
20024
|
+
...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
|
|
20025
|
+
...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
|
|
20026
|
+
...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
|
|
20027
|
+
...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
|
|
20028
|
+
...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
|
|
20029
|
+
...typeof sleeping === "boolean" ? { sleeping } : {},
|
|
20030
|
+
...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
|
|
20031
|
+
...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
|
|
19562
20032
|
};
|
|
19563
20033
|
});
|
|
19564
20034
|
const result = { channels, devices };
|
|
@@ -23826,13 +24296,12 @@ ${xml}`
|
|
|
23826
24296
|
]);
|
|
23827
24297
|
const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
|
|
23828
24298
|
const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
|
|
23829
|
-
const supportItem =
|
|
23830
|
-
const capabilities =
|
|
23831
|
-
ch,
|
|
23832
|
-
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
);
|
|
24299
|
+
const supportItem = getSupportItemForChannel(support, ch);
|
|
24300
|
+
const capabilities = computeDeviceCapabilities({
|
|
24301
|
+
channel: ch,
|
|
24302
|
+
...support != null && { support },
|
|
24303
|
+
...abilities != null && { abilities }
|
|
24304
|
+
});
|
|
23836
24305
|
const item = supportItem;
|
|
23837
24306
|
const lightType = item?.lightType;
|
|
23838
24307
|
const ledCtrl = item?.ledCtrl;
|
|
@@ -23848,6 +24317,25 @@ ${xml}`
|
|
|
23848
24317
|
});
|
|
23849
24318
|
capabilities.hasFloodlight = probed;
|
|
23850
24319
|
}
|
|
24320
|
+
let dingDongListIds;
|
|
24321
|
+
let dingDongCfgIds;
|
|
24322
|
+
let wirelessChimeError;
|
|
24323
|
+
if (capabilities.hasWirelessChime) {
|
|
24324
|
+
try {
|
|
24325
|
+
const list = await this.getDingDongList(ch);
|
|
24326
|
+
dingDongListIds = list.map((d) => d.id);
|
|
24327
|
+
const first = list[0];
|
|
24328
|
+
const fromList = first !== void 0 && first.id >= 0;
|
|
24329
|
+
if (!fromList) {
|
|
24330
|
+
const configs = await this.getDingDongCfg(ch);
|
|
24331
|
+
dingDongCfgIds = configs.map((c) => c.id);
|
|
24332
|
+
capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
|
|
24333
|
+
}
|
|
24334
|
+
} catch (e) {
|
|
24335
|
+
capabilities.hasWirelessChime = false;
|
|
24336
|
+
wirelessChimeError = e instanceof Error ? e.message : String(e);
|
|
24337
|
+
}
|
|
24338
|
+
}
|
|
23851
24339
|
const features = this.parseFeaturesFromSupport(support);
|
|
23852
24340
|
const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
|
|
23853
24341
|
const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
|
|
@@ -23884,7 +24372,10 @@ ${xml}`
|
|
|
23884
24372
|
...abilities && {
|
|
23885
24373
|
abilityMergedKeyCount: Object.keys(abilities).length
|
|
23886
24374
|
},
|
|
23887
|
-
...support?.items && { supportItemCount: support.items.length }
|
|
24375
|
+
...support?.items && { supportItemCount: support.items.length },
|
|
24376
|
+
...dingDongListIds !== void 0 && { dingDongListIds },
|
|
24377
|
+
...dingDongCfgIds !== void 0 && { dingDongCfgIds },
|
|
24378
|
+
...wirelessChimeError !== void 0 && { wirelessChimeError }
|
|
23888
24379
|
};
|
|
23889
24380
|
const result = {
|
|
23890
24381
|
capabilities,
|
|
@@ -23911,90 +24402,6 @@ ${xml}`
|
|
|
23911
24402
|
this.deviceCapabilitiesCache.clear();
|
|
23912
24403
|
}
|
|
23913
24404
|
}
|
|
23914
|
-
/**
|
|
23915
|
-
* Pick the best SupportItem for a channel.
|
|
23916
|
-
* Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
|
|
23917
|
-
*/
|
|
23918
|
-
pickBestSupportItem(support, channel) {
|
|
23919
|
-
if (!support?.items?.length) return void 0;
|
|
23920
|
-
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
23921
|
-
if (!candidates.length) return void 0;
|
|
23922
|
-
const score = (item) => {
|
|
23923
|
-
const anyItem = item;
|
|
23924
|
-
let result = 0;
|
|
23925
|
-
if (anyItem.name == null) result += 100;
|
|
23926
|
-
const capabilityKeys = [
|
|
23927
|
-
"ptzType",
|
|
23928
|
-
"ptzControl",
|
|
23929
|
-
"ptzPreset",
|
|
23930
|
-
"ledCtrl",
|
|
23931
|
-
"lightType",
|
|
23932
|
-
"battery",
|
|
23933
|
-
"audioVersion",
|
|
23934
|
-
"motion",
|
|
23935
|
-
"encCtrl",
|
|
23936
|
-
"newIspCfg",
|
|
23937
|
-
"remoteAbility",
|
|
23938
|
-
"aitype",
|
|
23939
|
-
"videoClip",
|
|
23940
|
-
"snap"
|
|
23941
|
-
];
|
|
23942
|
-
for (const k of capabilityKeys) {
|
|
23943
|
-
if (anyItem[k] !== void 0) result += 3;
|
|
23944
|
-
}
|
|
23945
|
-
return result;
|
|
23946
|
-
};
|
|
23947
|
-
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
23948
|
-
}
|
|
23949
|
-
/**
|
|
23950
|
-
* Parse device capabilities from SupportInfo.
|
|
23951
|
-
* Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
|
|
23952
|
-
*/
|
|
23953
|
-
parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
|
|
23954
|
-
const truthy = (v) => {
|
|
23955
|
-
if (typeof v === "number") return v > 0;
|
|
23956
|
-
if (typeof v === "string") {
|
|
23957
|
-
const n = Number(v);
|
|
23958
|
-
return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
|
|
23959
|
-
}
|
|
23960
|
-
return Boolean(v);
|
|
23961
|
-
};
|
|
23962
|
-
const item = supportItem;
|
|
23963
|
-
const ptzMode = support?.ptzMode?.toLowerCase();
|
|
23964
|
-
const ptzType = item ? truthy(item.ptzType) : false;
|
|
23965
|
-
const ptzControl = item ? truthy(item.ptzControl) : false;
|
|
23966
|
-
const hasPtzFromItem = ptzType || ptzControl;
|
|
23967
|
-
const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
|
|
23968
|
-
const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
|
|
23969
|
-
const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
|
|
23970
|
-
const hasPresets = item ? truthy(item.ptzPreset) : false;
|
|
23971
|
-
const hasBattery = item ? truthy(item.battery) : false;
|
|
23972
|
-
const hasSiren = item ? truthy(item.audioVersion) : false;
|
|
23973
|
-
const lightType = item?.lightType;
|
|
23974
|
-
const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
|
|
23975
|
-
const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
|
|
23976
|
-
const isDoorbell = item ? truthy(item.doorbellVersion) : false;
|
|
23977
|
-
const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
|
|
23978
|
-
return {
|
|
23979
|
-
channel,
|
|
23980
|
-
...ptzMode && { ptzMode },
|
|
23981
|
-
hasPan: hasPanTilt,
|
|
23982
|
-
hasTilt: hasPanTilt,
|
|
23983
|
-
hasZoom,
|
|
23984
|
-
hasPresets,
|
|
23985
|
-
hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
|
|
23986
|
-
hasBattery,
|
|
23987
|
-
hasIntercom,
|
|
23988
|
-
hasSiren,
|
|
23989
|
-
hasFloodlight,
|
|
23990
|
-
hasPir,
|
|
23991
|
-
isDoorbell,
|
|
23992
|
-
// Autotracking: explicit flags only (autoPt or smartAI)
|
|
23993
|
-
// Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
|
|
23994
|
-
// on cameras that have PTZ and AI detection but NOT autotracking capability.
|
|
23995
|
-
hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
|
|
23996
|
-
};
|
|
23997
|
-
}
|
|
23998
24405
|
/**
|
|
23999
24406
|
* Parse support features from SupportInfo.
|
|
24000
24407
|
*/
|
|
@@ -26867,6 +27274,216 @@ ${scheduleItems}
|
|
|
26867
27274
|
const channel = 0;
|
|
26868
27275
|
return await this.getSnapshot(channel);
|
|
26869
27276
|
}
|
|
27277
|
+
// --------------------
|
|
27278
|
+
// Chime / DingDong APIs
|
|
27279
|
+
// --------------------
|
|
27280
|
+
/**
|
|
27281
|
+
* Get the list of paired wireless chime devices.
|
|
27282
|
+
* cmd_id: 484 (GetDingDongList)
|
|
27283
|
+
*
|
|
27284
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27285
|
+
* @returns Array of paired chime devices
|
|
27286
|
+
*/
|
|
27287
|
+
async getDingDongList(channel) {
|
|
27288
|
+
const ch = this.normalizeChannel(channel);
|
|
27289
|
+
const xml = await this.sendXml({
|
|
27290
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
|
|
27291
|
+
channel: ch
|
|
27292
|
+
});
|
|
27293
|
+
return parseDingDongListFromXml(xml);
|
|
27294
|
+
}
|
|
27295
|
+
/**
|
|
27296
|
+
* Get parameters (name, volume, LED state) for a specific wireless chime.
|
|
27297
|
+
* cmd_id: 485 (DingDongOpt, option getParam)
|
|
27298
|
+
*
|
|
27299
|
+
* @param chimeId - The chime device ID
|
|
27300
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27301
|
+
* @returns Chime parameters
|
|
27302
|
+
*/
|
|
27303
|
+
async getDingDongParams(chimeId, channel) {
|
|
27304
|
+
const ch = this.normalizeChannel(channel);
|
|
27305
|
+
const payloadXml = buildDingDongGetParamsXml(chimeId);
|
|
27306
|
+
const xml = await this.sendXml({
|
|
27307
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
27308
|
+
channel: ch,
|
|
27309
|
+
payloadXml
|
|
27310
|
+
});
|
|
27311
|
+
return parseDingDongParamsFromXml(xml);
|
|
27312
|
+
}
|
|
27313
|
+
/**
|
|
27314
|
+
* Set parameters (name, volume, LED state) for a specific wireless chime.
|
|
27315
|
+
* cmd_id: 485 (DingDongOpt, option setParam)
|
|
27316
|
+
*
|
|
27317
|
+
* @param chimeId - The chime device ID
|
|
27318
|
+
* @param params - Parameters to set (volLevel, ledState, name)
|
|
27319
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27320
|
+
*/
|
|
27321
|
+
async setDingDongParams(chimeId, params, channel) {
|
|
27322
|
+
const ch = this.normalizeChannel(channel);
|
|
27323
|
+
const payloadXml = buildDingDongSetParamsXml(chimeId, params);
|
|
27324
|
+
await this.sendXml({
|
|
27325
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
27326
|
+
channel: ch,
|
|
27327
|
+
payloadXml
|
|
27328
|
+
});
|
|
27329
|
+
}
|
|
27330
|
+
/**
|
|
27331
|
+
* Trigger a wireless chime to ring with a specific ringtone.
|
|
27332
|
+
* cmd_id: 485 (DingDongOpt, option ringWithMusic)
|
|
27333
|
+
*
|
|
27334
|
+
* @param chimeId - The chime device ID
|
|
27335
|
+
* @param musicId - The ringtone/music ID to play
|
|
27336
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27337
|
+
*/
|
|
27338
|
+
async ringDingDong(chimeId, musicId, channel) {
|
|
27339
|
+
const ch = this.normalizeChannel(channel);
|
|
27340
|
+
const payloadXml = buildDingDongRingXml(chimeId, musicId);
|
|
27341
|
+
await this.sendXml({
|
|
27342
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
27343
|
+
channel: ch,
|
|
27344
|
+
payloadXml
|
|
27345
|
+
});
|
|
27346
|
+
}
|
|
27347
|
+
/**
|
|
27348
|
+
* Get the per-event alarm configuration for paired wireless chimes.
|
|
27349
|
+
* cmd_id: 486 (GetDingDongCfg)
|
|
27350
|
+
*
|
|
27351
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27352
|
+
* @returns Array of chime configurations (one per paired chime)
|
|
27353
|
+
*/
|
|
27354
|
+
async getDingDongCfg(channel) {
|
|
27355
|
+
const ch = this.normalizeChannel(channel);
|
|
27356
|
+
const xml = await this.sendXml({
|
|
27357
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
|
|
27358
|
+
channel: ch
|
|
27359
|
+
});
|
|
27360
|
+
return parseDingDongCfgFromXml(xml);
|
|
27361
|
+
}
|
|
27362
|
+
/**
|
|
27363
|
+
* Set the per-event alarm configuration for a specific wireless chime.
|
|
27364
|
+
* cmd_id: 487 (SetDingDongCfg)
|
|
27365
|
+
*
|
|
27366
|
+
* @param chimeId - The chime ring/device ID
|
|
27367
|
+
* @param eventType - Event type string (e.g. "doorbell", "package", "people")
|
|
27368
|
+
* @param state - 0 = disabled, 1 = enabled
|
|
27369
|
+
* @param musicId - Ringtone ID to use for this event type
|
|
27370
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27371
|
+
*/
|
|
27372
|
+
async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
|
|
27373
|
+
const ch = this.normalizeChannel(channel);
|
|
27374
|
+
const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
|
|
27375
|
+
await this.sendXml({
|
|
27376
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
|
|
27377
|
+
channel: ch,
|
|
27378
|
+
payloadXml
|
|
27379
|
+
});
|
|
27380
|
+
}
|
|
27381
|
+
/** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
|
|
27382
|
+
_hardwiredChimeCache = /* @__PURE__ */ new Map();
|
|
27383
|
+
/**
|
|
27384
|
+
* Get the hardwired (wired-in) chime state.
|
|
27385
|
+
* cmd_id: 483 (GetDingDongCtrl)
|
|
27386
|
+
*
|
|
27387
|
+
* Note: calling this may briefly trigger the physical chime to rattle.
|
|
27388
|
+
*
|
|
27389
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27390
|
+
* @returns Hardwired chime state (type, enabled, time)
|
|
27391
|
+
*/
|
|
27392
|
+
async getHardwiredChime(channel) {
|
|
27393
|
+
const ch = this.normalizeChannel(channel);
|
|
27394
|
+
const payloadXml = buildGetDingDongCtrlXml();
|
|
27395
|
+
const xml = await this.sendXml({
|
|
27396
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
27397
|
+
channel: ch,
|
|
27398
|
+
payloadXml
|
|
27399
|
+
});
|
|
27400
|
+
const state = parseHardwiredChimeFromXml(xml);
|
|
27401
|
+
this._hardwiredChimeCache.set(ch, state);
|
|
27402
|
+
return state;
|
|
27403
|
+
}
|
|
27404
|
+
/**
|
|
27405
|
+
* Set the hardwired (wired-in) chime state.
|
|
27406
|
+
* cmd_id: 483 (SetDingDongCtrl)
|
|
27407
|
+
*
|
|
27408
|
+
* Uses the cached state from a previous getHardwiredChime call to fill in
|
|
27409
|
+
* missing type/time fields, avoiding a double round-trip on every set.
|
|
27410
|
+
* Falls back to fetching if no cache is available.
|
|
27411
|
+
*
|
|
27412
|
+
* @param params - Chime configuration (type, enabled, time)
|
|
27413
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27414
|
+
*/
|
|
27415
|
+
async setHardwiredChime(params, channel) {
|
|
27416
|
+
const ch = this.normalizeChannel(channel);
|
|
27417
|
+
let current = this._hardwiredChimeCache.get(ch);
|
|
27418
|
+
if (!current) {
|
|
27419
|
+
current = await this.getHardwiredChime(ch);
|
|
27420
|
+
}
|
|
27421
|
+
const chimeType = params.type ?? current.type;
|
|
27422
|
+
const enabled = params.enabled ? 1 : 0;
|
|
27423
|
+
const time = params.time ?? current.time;
|
|
27424
|
+
const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
|
|
27425
|
+
const xml = await this.sendXml({
|
|
27426
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
27427
|
+
channel: ch,
|
|
27428
|
+
payloadXml
|
|
27429
|
+
});
|
|
27430
|
+
const newState = parseHardwiredChimeFromXml(xml);
|
|
27431
|
+
this._hardwiredChimeCache.set(ch, newState);
|
|
27432
|
+
return newState;
|
|
27433
|
+
}
|
|
27434
|
+
/**
|
|
27435
|
+
* Play an audio file on the doorbell / chime device.
|
|
27436
|
+
* cmd_id: 349 (QuickReplyPlay)
|
|
27437
|
+
*
|
|
27438
|
+
* @param fileId - The audio file ID to play
|
|
27439
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27440
|
+
*/
|
|
27441
|
+
async quickReplyPlay(fileId, channel) {
|
|
27442
|
+
const ch = this.normalizeChannel(channel);
|
|
27443
|
+
const payloadXml = buildQuickReplyPlayXml(ch, fileId);
|
|
27444
|
+
await this.sendXml({
|
|
27445
|
+
cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
27446
|
+
channel: ch,
|
|
27447
|
+
payloadXml
|
|
27448
|
+
});
|
|
27449
|
+
}
|
|
27450
|
+
/**
|
|
27451
|
+
* Get the silent mode state of a paired wireless chime.
|
|
27452
|
+
* cmd_id: 609 (GetDingDongSilent)
|
|
27453
|
+
*
|
|
27454
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
27455
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27456
|
+
* @returns Wireless chime silent state (time=0 means active/not silenced)
|
|
27457
|
+
*/
|
|
27458
|
+
async getDingDongSilent(chimeId, channel) {
|
|
27459
|
+
const ch = this.normalizeChannel(channel);
|
|
27460
|
+
const payloadXml = buildGetDingDongSilentXml(chimeId);
|
|
27461
|
+
const xml = await this.sendXml({
|
|
27462
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
27463
|
+
channel: ch,
|
|
27464
|
+
payloadXml
|
|
27465
|
+
});
|
|
27466
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
27467
|
+
}
|
|
27468
|
+
/**
|
|
27469
|
+
* Set the silent mode of a paired wireless chime.
|
|
27470
|
+
* cmd_id: 610 (SetDingDongSilent)
|
|
27471
|
+
*
|
|
27472
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
27473
|
+
* @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
|
|
27474
|
+
* @param channel - Channel number (0-based, default 0)
|
|
27475
|
+
* @returns Updated wireless chime silent state
|
|
27476
|
+
*/
|
|
27477
|
+
async setDingDongSilent(chimeId, time, channel) {
|
|
27478
|
+
const ch = this.normalizeChannel(channel);
|
|
27479
|
+
const payloadXml = buildSetDingDongSilentXml(chimeId, time);
|
|
27480
|
+
const xml = await this.sendXml({
|
|
27481
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
27482
|
+
channel: ch,
|
|
27483
|
+
payloadXml
|
|
27484
|
+
});
|
|
27485
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
27486
|
+
}
|
|
26870
27487
|
};
|
|
26871
27488
|
|
|
26872
27489
|
// src/reolink/baichuan/HlsSessionManager.ts
|
|
@@ -27219,8 +27836,12 @@ function detectIosClient(userAgent) {
|
|
|
27219
27836
|
return {
|
|
27220
27837
|
isIos,
|
|
27221
27838
|
isIosInstalledApp,
|
|
27222
|
-
// iOS
|
|
27223
|
-
|
|
27839
|
+
// ALL iOS clients need HLS for reliable video clip playback.
|
|
27840
|
+
// iOS AVFoundation requires Content-Length + Range support for regular MP4,
|
|
27841
|
+
// but generating the full MP4 upfront takes too long (camera download + transcode).
|
|
27842
|
+
// HLS delivers segments progressively (~3-5s to first frame vs 25+ seconds).
|
|
27843
|
+
// Safari, AVPlayer, and InstalledApp all support HLS natively.
|
|
27844
|
+
needsHls: isIos
|
|
27224
27845
|
};
|
|
27225
27846
|
}
|
|
27226
27847
|
function buildHlsRedirectUrl(originalUrl) {
|
|
@@ -34739,6 +35360,8 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
34739
35360
|
BC_CMD_ID_COVER_STANDALONE_460,
|
|
34740
35361
|
BC_CMD_ID_COVER_STANDALONE_461,
|
|
34741
35362
|
BC_CMD_ID_COVER_STANDALONE_462,
|
|
35363
|
+
BC_CMD_ID_DING_DONG_CTRL,
|
|
35364
|
+
BC_CMD_ID_DING_DONG_OPT,
|
|
34742
35365
|
BC_CMD_ID_FILE_INFO_LIST_CLOSE,
|
|
34743
35366
|
BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO,
|
|
34744
35367
|
BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD,
|
|
@@ -34762,6 +35385,9 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
34762
35385
|
BC_CMD_ID_GET_BATTERY_INFO_LIST,
|
|
34763
35386
|
BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
34764
35387
|
BC_CMD_ID_GET_DAY_RECORDS,
|
|
35388
|
+
BC_CMD_ID_GET_DING_DONG_CFG,
|
|
35389
|
+
BC_CMD_ID_GET_DING_DONG_LIST,
|
|
35390
|
+
BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
34765
35391
|
BC_CMD_ID_GET_EMAIL_TASK,
|
|
34766
35392
|
BC_CMD_ID_GET_FTP_TASK,
|
|
34767
35393
|
BC_CMD_ID_GET_HDD_INFO_LIST,
|
|
@@ -34798,9 +35424,12 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
34798
35424
|
BC_CMD_ID_PUSH_SERIAL,
|
|
34799
35425
|
BC_CMD_ID_PUSH_SLEEP_STATUS,
|
|
34800
35426
|
BC_CMD_ID_PUSH_VIDEO_INPUT,
|
|
35427
|
+
BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
34801
35428
|
BC_CMD_ID_SET_AI_ALARM,
|
|
34802
35429
|
BC_CMD_ID_SET_AI_CFG,
|
|
34803
35430
|
BC_CMD_ID_SET_AUDIO_TASK,
|
|
35431
|
+
BC_CMD_ID_SET_DING_DONG_CFG,
|
|
35432
|
+
BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
34804
35433
|
BC_CMD_ID_SET_MOTION_ALARM,
|
|
34805
35434
|
BC_CMD_ID_SET_PIR_INFO,
|
|
34806
35435
|
BC_CMD_ID_SET_WHITE_LED_STATE,
|
|
@@ -34917,6 +35546,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
34917
35546
|
getGlobalLogger,
|
|
34918
35547
|
getH265NalType,
|
|
34919
35548
|
getMjpegContentType,
|
|
35549
|
+
getSupportItemForChannel,
|
|
34920
35550
|
getVideoStream,
|
|
34921
35551
|
getVideoclipClientInfo,
|
|
34922
35552
|
getXmlText,
|