@apocaliss92/nodelink-js 0.1.20 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/{DiagnosticsTools-NUMCYEKQ.js → DiagnosticsTools-FNLGCOVA.js} +2 -2
- package/dist/{chunk-EHWVA3SG.js → chunk-MN7GUZT7.js} +981 -250
- package/dist/chunk-MN7GUZT7.js.map +1 -0
- package/dist/{chunk-YPU7RAEY.js → chunk-NLTB7GTA.js} +17 -1
- package/dist/{chunk-YPU7RAEY.js.map → chunk-NLTB7GTA.js.map} +1 -1
- package/dist/cli/rtsp-server.cjs +978 -247
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +1002 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -16
- package/dist/index.d.ts +260 -15
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-EHWVA3SG.js.map +0 -1
- /package/dist/{DiagnosticsTools-NUMCYEKQ.js.map → DiagnosticsTools-FNLGCOVA.js.map} +0 -0
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
BC_CMD_ID_CMD_209,
|
|
11
11
|
BC_CMD_ID_CMD_265,
|
|
12
12
|
BC_CMD_ID_CMD_440,
|
|
13
|
+
BC_CMD_ID_DING_DONG_CTRL,
|
|
14
|
+
BC_CMD_ID_DING_DONG_OPT,
|
|
13
15
|
BC_CMD_ID_FILE_INFO_LIST_CLOSE,
|
|
14
16
|
BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO,
|
|
15
17
|
BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD,
|
|
@@ -33,6 +35,9 @@ import {
|
|
|
33
35
|
BC_CMD_ID_GET_BATTERY_INFO_LIST,
|
|
34
36
|
BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
35
37
|
BC_CMD_ID_GET_DAY_RECORDS,
|
|
38
|
+
BC_CMD_ID_GET_DING_DONG_CFG,
|
|
39
|
+
BC_CMD_ID_GET_DING_DONG_LIST,
|
|
40
|
+
BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
36
41
|
BC_CMD_ID_GET_EMAIL_TASK,
|
|
37
42
|
BC_CMD_ID_GET_FTP_TASK,
|
|
38
43
|
BC_CMD_ID_GET_HDD_INFO_LIST,
|
|
@@ -68,9 +73,12 @@ import {
|
|
|
68
73
|
BC_CMD_ID_PUSH_SERIAL,
|
|
69
74
|
BC_CMD_ID_PUSH_SLEEP_STATUS,
|
|
70
75
|
BC_CMD_ID_PUSH_VIDEO_INPUT,
|
|
76
|
+
BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
71
77
|
BC_CMD_ID_SET_AI_ALARM,
|
|
72
78
|
BC_CMD_ID_SET_AI_CFG,
|
|
73
79
|
BC_CMD_ID_SET_AUDIO_TASK,
|
|
80
|
+
BC_CMD_ID_SET_DING_DONG_CFG,
|
|
81
|
+
BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
74
82
|
BC_CMD_ID_SET_MOTION_ALARM,
|
|
75
83
|
BC_CMD_ID_SET_PIR_INFO,
|
|
76
84
|
BC_CMD_ID_SET_WHITE_LED_STATE,
|
|
@@ -136,7 +144,7 @@ import {
|
|
|
136
144
|
talkTraceLog,
|
|
137
145
|
traceLog,
|
|
138
146
|
xmlEscape
|
|
139
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-NLTB7GTA.js";
|
|
140
148
|
|
|
141
149
|
// src/protocol/framing.ts
|
|
142
150
|
function encodeHeader(h) {
|
|
@@ -1807,6 +1815,13 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1807
1815
|
* and may leave device-side sessions in a bad state.
|
|
1808
1816
|
*/
|
|
1809
1817
|
static coverPreviewQueueTail = /* @__PURE__ */ new Map();
|
|
1818
|
+
/**
|
|
1819
|
+
* Global CoverPreview backoff – increases on 400 rejection, resets on success.
|
|
1820
|
+
* Prevents flooding the camera when it's overwhelmed.
|
|
1821
|
+
*/
|
|
1822
|
+
static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
|
|
1823
|
+
static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
|
|
1824
|
+
static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
|
|
1810
1825
|
opts;
|
|
1811
1826
|
debugCfg;
|
|
1812
1827
|
logger;
|
|
@@ -1954,7 +1969,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1954
1969
|
if (!this.isIdleDisconnectEnabled()) return false;
|
|
1955
1970
|
if (!this.isSocketConnected()) return false;
|
|
1956
1971
|
if (this.pending.size > 0) return false;
|
|
1957
|
-
if (this.
|
|
1972
|
+
if (this.isDeviceStreamingActive()) return false;
|
|
1958
1973
|
if (this.permits.size > 0) return false;
|
|
1959
1974
|
return true;
|
|
1960
1975
|
}
|
|
@@ -1968,7 +1983,18 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
1968
1983
|
const delayMs = Math.max(0, timeoutMs - elapsedMs);
|
|
1969
1984
|
this.idleDisconnectTimer = setTimeout(() => {
|
|
1970
1985
|
try {
|
|
1971
|
-
if (!this.isIdleDisconnectEligibleNow())
|
|
1986
|
+
if (!this.isIdleDisconnectEligibleNow()) {
|
|
1987
|
+
this.logDebug("idle_disconnect_blocked", {
|
|
1988
|
+
reason: "not eligible",
|
|
1989
|
+
socketConnected: this.isSocketConnected(),
|
|
1990
|
+
pending: this.pending.size,
|
|
1991
|
+
deviceStreamingActive: this.isDeviceStreamingActive(),
|
|
1992
|
+
localVideoSubs: this.hasActiveVideoSubscriptionsInternal(),
|
|
1993
|
+
permits: this.permits.size,
|
|
1994
|
+
host: this.opts.host
|
|
1995
|
+
});
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1972
1998
|
if (this.lastUserActivityAtMs == null) return;
|
|
1973
1999
|
const elapsed2 = Date.now() - this.lastUserActivityAtMs;
|
|
1974
2000
|
if (elapsed2 < timeoutMs) {
|
|
@@ -2082,7 +2108,34 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
2082
2108
|
async withSerializedCoverPreview(fn) {
|
|
2083
2109
|
const key = this.getCoverPreviewQueueKey();
|
|
2084
2110
|
const prevTail = _BaichuanClient.coverPreviewQueueTail.get(key) ?? Promise.resolve();
|
|
2085
|
-
const run = prevTail.catch(() => void 0).then(
|
|
2111
|
+
const run = prevTail.catch(() => void 0).then(async () => {
|
|
2112
|
+
const backoffMs = _BaichuanClient.coverPreviewBackoffMs.get(key) ?? 0;
|
|
2113
|
+
if (backoffMs > 0) {
|
|
2114
|
+
this.logDebug("coverpreview_backoff_wait", { backoffMs });
|
|
2115
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
2116
|
+
}
|
|
2117
|
+
try {
|
|
2118
|
+
const result = await fn();
|
|
2119
|
+
_BaichuanClient.coverPreviewBackoffMs.delete(key);
|
|
2120
|
+
return result;
|
|
2121
|
+
} catch (e) {
|
|
2122
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2123
|
+
const is400 = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
|
|
2124
|
+
if (is400) {
|
|
2125
|
+
const current = _BaichuanClient.coverPreviewBackoffMs.get(key) ?? 0;
|
|
2126
|
+
const next = current === 0 ? _BaichuanClient.COVER_PREVIEW_INITIAL_BACKOFF_MS : Math.min(
|
|
2127
|
+
current * 2,
|
|
2128
|
+
_BaichuanClient.COVER_PREVIEW_MAX_BACKOFF_MS
|
|
2129
|
+
);
|
|
2130
|
+
_BaichuanClient.coverPreviewBackoffMs.set(key, next);
|
|
2131
|
+
this.logDebug("coverpreview_backoff_increased", {
|
|
2132
|
+
previous: current,
|
|
2133
|
+
next
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
throw e;
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2086
2139
|
const tail = run.then(
|
|
2087
2140
|
() => void 0,
|
|
2088
2141
|
() => void 0
|
|
@@ -4404,32 +4457,36 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
|
|
|
4404
4457
|
* Send CoverPreview command (cmd_id=298) to get an I-frame from a past recording.
|
|
4405
4458
|
* Similar to sendBinarySnapshot109 but handles the stream header + frame format
|
|
4406
4459
|
* instead of JPEG.
|
|
4460
|
+
*
|
|
4461
|
+
* Retry is minimal (2 attempts) – the global backoff in `withSerializedCoverPreview`
|
|
4462
|
+
* throttles subsequent requests when the camera is overwhelmed.
|
|
4463
|
+
* PCAP analysis shows the camera routinely rejects the first request with 400.
|
|
4407
4464
|
*/
|
|
4408
4465
|
async sendBinaryCoverPreview(params) {
|
|
4409
4466
|
return await this.withSerializedCoverPreview(async () => {
|
|
4410
|
-
const
|
|
4411
|
-
const
|
|
4467
|
+
const maxAttempts = 5;
|
|
4468
|
+
const retryDelay = 1500;
|
|
4412
4469
|
let lastError;
|
|
4413
|
-
for (let attempt = 0; attempt <
|
|
4470
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4414
4471
|
try {
|
|
4415
4472
|
return await this._sendBinaryCoverPreviewOnce(params);
|
|
4416
4473
|
} catch (e) {
|
|
4417
4474
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4418
4475
|
lastError = e instanceof Error ? e : new Error(msg);
|
|
4419
|
-
const
|
|
4420
|
-
if (
|
|
4476
|
+
const is400 = msg.includes("rejected") && (msg.includes("responseCode=400") || msg.includes("resp_code=400"));
|
|
4477
|
+
if (is400 && attempt < maxAttempts - 1) {
|
|
4421
4478
|
this.logDebug("coverpreview_retry_400", {
|
|
4422
4479
|
attempt: attempt + 1,
|
|
4423
|
-
|
|
4424
|
-
|
|
4480
|
+
maxAttempts,
|
|
4481
|
+
retryDelay
|
|
4425
4482
|
});
|
|
4426
|
-
await new Promise((
|
|
4483
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
4427
4484
|
continue;
|
|
4428
4485
|
}
|
|
4429
4486
|
throw lastError;
|
|
4430
4487
|
}
|
|
4431
4488
|
}
|
|
4432
|
-
throw lastError ?? new Error("CoverPreview failed after all
|
|
4489
|
+
throw lastError ?? new Error("CoverPreview failed after all attempts");
|
|
4433
4490
|
});
|
|
4434
4491
|
}
|
|
4435
4492
|
/**
|
|
@@ -5343,6 +5400,8 @@ var NativeStreamFanout = class {
|
|
|
5343
5400
|
} finally {
|
|
5344
5401
|
for (const q of this.queues.values()) q.close();
|
|
5345
5402
|
this.queues.clear();
|
|
5403
|
+
this.running = false;
|
|
5404
|
+
this.opts.onEnd?.();
|
|
5346
5405
|
}
|
|
5347
5406
|
})();
|
|
5348
5407
|
}
|
|
@@ -5668,7 +5727,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5668
5727
|
this.logger.warn(
|
|
5669
5728
|
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
5670
5729
|
);
|
|
5671
|
-
this.streamMetadata = { frameRate: 25
|
|
5730
|
+
this.streamMetadata = { frameRate: 25 };
|
|
5672
5731
|
this.setFlowVideoType("H264", "metadata unavailable");
|
|
5673
5732
|
}
|
|
5674
5733
|
this.clientConnectionServer = net2.createServer((socket) => {
|
|
@@ -5700,7 +5759,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5700
5759
|
*/
|
|
5701
5760
|
handleRtspConnection(socket) {
|
|
5702
5761
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
5703
|
-
|
|
5762
|
+
const connectTime = Date.now();
|
|
5763
|
+
this.logger.info(
|
|
5764
|
+
`[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
|
|
5765
|
+
);
|
|
5704
5766
|
let sessionId = "";
|
|
5705
5767
|
let buffer = Buffer.alloc(0);
|
|
5706
5768
|
let clientFfmpeg;
|
|
@@ -5708,6 +5770,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5708
5770
|
let clientUdpSocket = null;
|
|
5709
5771
|
let clientUdpSocketAudio = null;
|
|
5710
5772
|
const cleanup = () => {
|
|
5773
|
+
const sessionDurationMs = Date.now() - connectTime;
|
|
5774
|
+
const res = this.clientResources.get(clientId);
|
|
5775
|
+
const framesSent = res?.framesSent ?? 0;
|
|
5776
|
+
this.logger.info(
|
|
5777
|
+
`[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
|
|
5778
|
+
);
|
|
5711
5779
|
this.removeClient(clientId);
|
|
5712
5780
|
this.authNonces.delete(clientId);
|
|
5713
5781
|
const resources = this.clientResources.get(clientId);
|
|
@@ -5849,7 +5917,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5849
5917
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
5850
5918
|
});
|
|
5851
5919
|
} else if (method === "DESCRIBE") {
|
|
5852
|
-
if (!this.
|
|
5920
|
+
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
5853
5921
|
try {
|
|
5854
5922
|
if (!this.nativeStreamActive) {
|
|
5855
5923
|
await this.startNativeStream();
|
|
@@ -5931,7 +5999,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5931
5999
|
seenFirstVideoKeyframe: false,
|
|
5932
6000
|
setupTrack0: false,
|
|
5933
6001
|
setupTrack1: false,
|
|
5934
|
-
isPlaying: false
|
|
6002
|
+
isPlaying: false,
|
|
6003
|
+
connectTime
|
|
5935
6004
|
});
|
|
5936
6005
|
} else {
|
|
5937
6006
|
existing.rtspSocket = socket;
|
|
@@ -5978,8 +6047,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5978
6047
|
if (resources) {
|
|
5979
6048
|
if (isTrack1) resources.setupTrack1 = true;
|
|
5980
6049
|
else resources.setupTrack0 = true;
|
|
5981
|
-
|
|
5982
|
-
|
|
6050
|
+
const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
|
|
6051
|
+
const track = isTrack1 ? "track1(audio)" : "track0(video)";
|
|
6052
|
+
this.logger.info(
|
|
6053
|
+
`[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
|
|
5983
6054
|
);
|
|
5984
6055
|
}
|
|
5985
6056
|
}
|
|
@@ -6004,8 +6075,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6004
6075
|
const resources = this.clientResources.get(clientId);
|
|
6005
6076
|
if (resources) {
|
|
6006
6077
|
resources.isPlaying = true;
|
|
6007
|
-
|
|
6008
|
-
|
|
6078
|
+
const hasAudio = !!resources.setupTrack1;
|
|
6079
|
+
this.logger.info(
|
|
6080
|
+
`[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
|
|
6009
6081
|
);
|
|
6010
6082
|
}
|
|
6011
6083
|
}
|
|
@@ -6014,6 +6086,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6014
6086
|
Range: "npt=0.000-"
|
|
6015
6087
|
});
|
|
6016
6088
|
} else if (method === "TEARDOWN") {
|
|
6089
|
+
this.logger.info(
|
|
6090
|
+
`[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
|
|
6091
|
+
);
|
|
6017
6092
|
cleanup();
|
|
6018
6093
|
sendResponse(200, "OK", {
|
|
6019
6094
|
Session: sessionId
|
|
@@ -6108,7 +6183,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6108
6183
|
this.logger.warn(
|
|
6109
6184
|
`[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
|
|
6110
6185
|
);
|
|
6111
|
-
streamMetadata = { frameRate: 25
|
|
6186
|
+
streamMetadata = { frameRate: 25 };
|
|
6112
6187
|
}
|
|
6113
6188
|
}
|
|
6114
6189
|
const ffmpegFormat = this.flow.ffmpegFormat;
|
|
@@ -6696,15 +6771,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6696
6771
|
`Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
|
|
6697
6772
|
);
|
|
6698
6773
|
}
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6774
|
+
if (!useDirectRtp) {
|
|
6775
|
+
const now = Date.now();
|
|
6776
|
+
const timeSinceLastFrame = now - lastFrameTime;
|
|
6777
|
+
const waitTime = targetFrameInterval - timeSinceLastFrame;
|
|
6778
|
+
if (waitTime > 0) {
|
|
6779
|
+
await new Promise(
|
|
6780
|
+
(resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
|
|
6781
|
+
);
|
|
6782
|
+
}
|
|
6783
|
+
lastFrameTime = Date.now();
|
|
6706
6784
|
}
|
|
6707
|
-
lastFrameTime = Date.now();
|
|
6708
6785
|
if (useDirectRtp) {
|
|
6709
6786
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
6710
6787
|
const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
|
|
@@ -6777,6 +6854,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6777
6854
|
}
|
|
6778
6855
|
if (!firstVideoWriteLogged) {
|
|
6779
6856
|
firstVideoWriteLogged = true;
|
|
6857
|
+
const clientConnectTime = resources?.connectTime ?? Date.now();
|
|
6858
|
+
const ttffMs = Date.now() - clientConnectTime;
|
|
6859
|
+
this.logger.info(
|
|
6860
|
+
`[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
|
|
6861
|
+
);
|
|
6780
6862
|
if (rtspDebug) {
|
|
6781
6863
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
6782
6864
|
rtspDebugLog(
|
|
@@ -6784,6 +6866,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6784
6866
|
);
|
|
6785
6867
|
}
|
|
6786
6868
|
}
|
|
6869
|
+
if (resources) {
|
|
6870
|
+
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
6871
|
+
}
|
|
6787
6872
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
6788
6873
|
} else {
|
|
6789
6874
|
try {
|
|
@@ -6868,8 +6953,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6868
6953
|
this.firstAudioPromise = new Promise((resolve) => {
|
|
6869
6954
|
this.firstAudioResolve = resolve;
|
|
6870
6955
|
});
|
|
6871
|
-
this.
|
|
6872
|
-
`
|
|
6956
|
+
this.logger.info(
|
|
6957
|
+
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
6873
6958
|
);
|
|
6874
6959
|
await this.flow.startKeepAlive(this.api);
|
|
6875
6960
|
this.nativeFanout = new NativeStreamFanout({
|
|
@@ -6912,6 +6997,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6912
6997
|
this.logger.warn(
|
|
6913
6998
|
`[BaichuanRtspServer] Shared native stream error: ${error}`
|
|
6914
6999
|
);
|
|
7000
|
+
},
|
|
7001
|
+
onEnd: () => {
|
|
7002
|
+
if (!this.nativeStreamActive) return;
|
|
7003
|
+
this.nativeStreamActive = false;
|
|
7004
|
+
this.firstFrameReceived = false;
|
|
7005
|
+
this.firstFramePromise = null;
|
|
7006
|
+
this.firstFrameResolve = null;
|
|
7007
|
+
this.nativeFanout = null;
|
|
7008
|
+
this.logger.info(
|
|
7009
|
+
`[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
7010
|
+
);
|
|
7011
|
+
if (this.connectedClients.size > 0) {
|
|
7012
|
+
this.logger.info(
|
|
7013
|
+
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
7014
|
+
);
|
|
7015
|
+
setImmediate(() => void this.startNativeStream());
|
|
7016
|
+
}
|
|
6915
7017
|
}
|
|
6916
7018
|
});
|
|
6917
7019
|
this.nativeFanout.start();
|
|
@@ -6950,7 +7052,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6950
7052
|
if (!this.nativeStreamActive) {
|
|
6951
7053
|
return;
|
|
6952
7054
|
}
|
|
6953
|
-
this.
|
|
7055
|
+
this.logger.info(
|
|
7056
|
+
`[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
7057
|
+
);
|
|
6954
7058
|
this.flow.stopKeepAlive();
|
|
6955
7059
|
this.clearNoClientAutoStopTimer();
|
|
6956
7060
|
this.nativeStreamActive = false;
|
|
@@ -6984,9 +7088,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6984
7088
|
if (this.connectedClients.has(clientId)) {
|
|
6985
7089
|
this.connectedClients.delete(clientId);
|
|
6986
7090
|
this.emit("clientDisconnected", clientId);
|
|
6987
|
-
this.logger.info(
|
|
6988
|
-
`[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
|
|
6989
|
-
);
|
|
6990
7091
|
if (this.connectedClients.size === 0) {
|
|
6991
7092
|
void this.stopNativeStream();
|
|
6992
7093
|
}
|
|
@@ -7219,10 +7320,12 @@ function parseSupportXml(xml) {
|
|
|
7219
7320
|
}
|
|
7220
7321
|
function getSupportItemForChannel(support, channel) {
|
|
7221
7322
|
if (!support?.items?.length) return void 0;
|
|
7222
|
-
const
|
|
7323
|
+
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
7324
|
+
if (!candidates.length) return void 0;
|
|
7325
|
+
const score = (item) => {
|
|
7223
7326
|
const anyItem = item;
|
|
7224
|
-
let
|
|
7225
|
-
if (anyItem.name == null)
|
|
7327
|
+
let result = 0;
|
|
7328
|
+
if (anyItem.name == null) result += 100;
|
|
7226
7329
|
const capabilityKeys = [
|
|
7227
7330
|
"ptzType",
|
|
7228
7331
|
"ptzControl",
|
|
@@ -7234,20 +7337,17 @@ function getSupportItemForChannel(support, channel) {
|
|
|
7234
7337
|
"motion",
|
|
7235
7338
|
"encCtrl",
|
|
7236
7339
|
"newIspCfg",
|
|
7237
|
-
"remoteAbility"
|
|
7340
|
+
"remoteAbility",
|
|
7341
|
+
"aitype",
|
|
7342
|
+
"videoClip",
|
|
7343
|
+
"snap"
|
|
7238
7344
|
];
|
|
7239
7345
|
for (const k of capabilityKeys) {
|
|
7240
|
-
if (anyItem[k] !== void 0)
|
|
7346
|
+
if (anyItem[k] !== void 0) result += 3;
|
|
7241
7347
|
}
|
|
7242
|
-
|
|
7243
|
-
return score;
|
|
7244
|
-
};
|
|
7245
|
-
const pickBest = (chnId) => {
|
|
7246
|
-
const candidates = support.items.filter((i) => i.chnID === chnId);
|
|
7247
|
-
if (!candidates.length) return void 0;
|
|
7248
|
-
return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
|
|
7348
|
+
return result;
|
|
7249
7349
|
};
|
|
7250
|
-
return
|
|
7350
|
+
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
7251
7351
|
}
|
|
7252
7352
|
function computeDeviceCapabilities(params) {
|
|
7253
7353
|
const { channel } = params;
|
|
@@ -7279,6 +7379,7 @@ function computeDeviceCapabilities(params) {
|
|
|
7279
7379
|
flat,
|
|
7280
7380
|
/white\s*led|whiteLed|flood\s*light|floodlight/i
|
|
7281
7381
|
);
|
|
7382
|
+
const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
|
|
7282
7383
|
const hasSirenFromAbilities = abilitiesHasAny(
|
|
7283
7384
|
flat,
|
|
7284
7385
|
/audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
|
|
@@ -7291,6 +7392,9 @@ function computeDeviceCapabilities(params) {
|
|
|
7291
7392
|
const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
|
|
7292
7393
|
const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
|
|
7293
7394
|
const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
|
|
7395
|
+
const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
|
|
7396
|
+
const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
|
|
7397
|
+
const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
|
|
7294
7398
|
const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
7295
7399
|
const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
7296
7400
|
const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
|
|
@@ -7306,14 +7410,15 @@ function computeDeviceCapabilities(params) {
|
|
|
7306
7410
|
hasZoom: finalHasZoom,
|
|
7307
7411
|
hasPresets: finalHasPresets,
|
|
7308
7412
|
hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
|
|
7309
|
-
hasBattery
|
|
7413
|
+
hasBattery,
|
|
7310
7414
|
hasIntercom: hasIntercomFromSupport,
|
|
7311
|
-
hasSiren: hasSirenFromAbilities,
|
|
7415
|
+
hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
|
|
7312
7416
|
// lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
|
|
7313
7417
|
hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
|
|
7314
7418
|
hasPir: hasPirFromAbilities || hasPirFromSupport,
|
|
7315
|
-
isDoorbell
|
|
7316
|
-
hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
|
|
7419
|
+
isDoorbell,
|
|
7420
|
+
hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
|
|
7421
|
+
hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
|
|
7317
7422
|
};
|
|
7318
7423
|
if (ptzMode !== void 0) result.ptzMode = ptzMode;
|
|
7319
7424
|
return result;
|
|
@@ -9188,6 +9293,161 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
|
|
|
9188
9293
|
return extractReolinkUidLike(p2pXml);
|
|
9189
9294
|
};
|
|
9190
9295
|
|
|
9296
|
+
// src/reolink/baichuan/utils/chime.ts
|
|
9297
|
+
var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9298
|
+
<body>
|
|
9299
|
+
<dingdongDeviceOpt version="1.1">
|
|
9300
|
+
<id>${chimeId}</id>
|
|
9301
|
+
<opt>getParam</opt>
|
|
9302
|
+
</dingdongDeviceOpt>
|
|
9303
|
+
</body>`;
|
|
9304
|
+
var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9305
|
+
<body>
|
|
9306
|
+
<dingdongDeviceOpt version="1.1">
|
|
9307
|
+
<opt>setParam</opt>
|
|
9308
|
+
<id>${chimeId}</id>
|
|
9309
|
+
${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
|
|
9310
|
+
${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
|
|
9311
|
+
${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
|
|
9312
|
+
</dingdongDeviceOpt>
|
|
9313
|
+
</body>`;
|
|
9314
|
+
var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9315
|
+
<body>
|
|
9316
|
+
<dingdongDeviceOpt version="1.1">
|
|
9317
|
+
<id>${chimeId}</id>
|
|
9318
|
+
<opt>ringWithMusic</opt>
|
|
9319
|
+
<musicId>${musicId}</musicId>
|
|
9320
|
+
</dingdongDeviceOpt>
|
|
9321
|
+
</body>`;
|
|
9322
|
+
var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9323
|
+
<body>
|
|
9324
|
+
<dingdongCfg version="1.1">
|
|
9325
|
+
<deviceCfg>
|
|
9326
|
+
<id>${chimeId}</id>
|
|
9327
|
+
<alarminCfg>
|
|
9328
|
+
<valid>${state}</valid>
|
|
9329
|
+
<musicId>${musicId}</musicId>
|
|
9330
|
+
<type>${eventType}</type>
|
|
9331
|
+
</alarminCfg>
|
|
9332
|
+
</deviceCfg>
|
|
9333
|
+
</dingdongCfg>
|
|
9334
|
+
</body>`;
|
|
9335
|
+
var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9336
|
+
<body>
|
|
9337
|
+
<dingdongCtrl version="1.1">
|
|
9338
|
+
<opt>machineStateGet</opt>
|
|
9339
|
+
</dingdongCtrl>
|
|
9340
|
+
</body>`;
|
|
9341
|
+
var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9342
|
+
<body>
|
|
9343
|
+
<dingdongCtrl version="1.1">
|
|
9344
|
+
<opt>machineStateSet</opt>
|
|
9345
|
+
<type>${chimeType}</type>
|
|
9346
|
+
<bopen>${enabled}</bopen>
|
|
9347
|
+
<bsave>1</bsave>
|
|
9348
|
+
<time>${time}</time>
|
|
9349
|
+
</dingdongCtrl>
|
|
9350
|
+
</body>`;
|
|
9351
|
+
var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9352
|
+
<body>
|
|
9353
|
+
<audioFileInfo version="1.1">
|
|
9354
|
+
<channelId>${channel}</channelId>
|
|
9355
|
+
<id>${fileId}</id>
|
|
9356
|
+
<timeout>0</timeout>
|
|
9357
|
+
</audioFileInfo>
|
|
9358
|
+
</body>`;
|
|
9359
|
+
var parseDingDongListFromXml = (xml) => {
|
|
9360
|
+
const devices = [];
|
|
9361
|
+
const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
|
|
9362
|
+
for (const block of blocks) {
|
|
9363
|
+
const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
|
|
9364
|
+
const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
|
|
9365
|
+
const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
|
|
9366
|
+
if (idText === void 0) continue;
|
|
9367
|
+
const id = Number(idText);
|
|
9368
|
+
if (!Number.isFinite(id)) continue;
|
|
9369
|
+
devices.push({
|
|
9370
|
+
id,
|
|
9371
|
+
name,
|
|
9372
|
+
netState: netStateText !== void 0 ? Number(netStateText) : 0
|
|
9373
|
+
});
|
|
9374
|
+
}
|
|
9375
|
+
return devices;
|
|
9376
|
+
};
|
|
9377
|
+
var parseDingDongParamsFromXml = (xml) => {
|
|
9378
|
+
const name = getXmlText(xml, "name");
|
|
9379
|
+
const volLevelText = getXmlText(xml, "volLevel");
|
|
9380
|
+
const ledStateText = getXmlText(xml, "ledState");
|
|
9381
|
+
const result = {};
|
|
9382
|
+
if (name !== void 0) result.name = name;
|
|
9383
|
+
if (volLevelText !== void 0) {
|
|
9384
|
+
const n = Number(volLevelText);
|
|
9385
|
+
if (Number.isFinite(n)) result.volLevel = n;
|
|
9386
|
+
}
|
|
9387
|
+
if (ledStateText !== void 0) {
|
|
9388
|
+
const n = Number(ledStateText);
|
|
9389
|
+
if (Number.isFinite(n)) result.ledState = n;
|
|
9390
|
+
}
|
|
9391
|
+
return result;
|
|
9392
|
+
};
|
|
9393
|
+
var parseDingDongCfgFromXml = (xml) => {
|
|
9394
|
+
const configs = [];
|
|
9395
|
+
const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
|
|
9396
|
+
for (const deviceBlock of deviceBlocks) {
|
|
9397
|
+
const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
|
|
9398
|
+
if (idText === void 0) continue;
|
|
9399
|
+
const id = Number(idText);
|
|
9400
|
+
if (!Number.isFinite(id)) continue;
|
|
9401
|
+
const typeMap = {};
|
|
9402
|
+
const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
|
|
9403
|
+
for (const alarmBlock of alarmBlocks) {
|
|
9404
|
+
const type = getXmlText(alarmBlock, "type");
|
|
9405
|
+
if (!type) continue;
|
|
9406
|
+
const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
|
|
9407
|
+
const musicIdText = getXmlText(alarmBlock, "musicId");
|
|
9408
|
+
typeMap[type] = {
|
|
9409
|
+
valid: validText !== void 0 ? Number(validText) : 0,
|
|
9410
|
+
musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
|
|
9411
|
+
};
|
|
9412
|
+
}
|
|
9413
|
+
configs.push({ id, type: typeMap });
|
|
9414
|
+
}
|
|
9415
|
+
return configs;
|
|
9416
|
+
};
|
|
9417
|
+
var parseHardwiredChimeFromXml = (xml) => {
|
|
9418
|
+
const type = getXmlText(xml, "type") ?? "";
|
|
9419
|
+
const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
|
|
9420
|
+
const timeText = getXmlText(xml, "time");
|
|
9421
|
+
return {
|
|
9422
|
+
type,
|
|
9423
|
+
enabled: bopenText === "1",
|
|
9424
|
+
time: timeText !== void 0 ? Number(timeText) : 0
|
|
9425
|
+
};
|
|
9426
|
+
};
|
|
9427
|
+
var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9428
|
+
<body>
|
|
9429
|
+
<dingdongSilentMode version="1.1">
|
|
9430
|
+
<id>${chimeId}</id>
|
|
9431
|
+
</dingdongSilentMode>
|
|
9432
|
+
</body>`;
|
|
9433
|
+
var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9434
|
+
<body>
|
|
9435
|
+
<dingdongSilentMode version="1.1">
|
|
9436
|
+
<id>${chimeId}</id>
|
|
9437
|
+
<time>${time}</time>
|
|
9438
|
+
<type>63</type>
|
|
9439
|
+
</dingdongSilentMode>
|
|
9440
|
+
</body>`;
|
|
9441
|
+
var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
|
|
9442
|
+
const timeText = getXmlText(xml, "time");
|
|
9443
|
+
const time = timeText !== void 0 ? Number(timeText) : 0;
|
|
9444
|
+
return {
|
|
9445
|
+
id: chimeId,
|
|
9446
|
+
time,
|
|
9447
|
+
active: time === 0
|
|
9448
|
+
};
|
|
9449
|
+
};
|
|
9450
|
+
|
|
9191
9451
|
// src/reolink/baichuan/utils/eventsGetEvents.ts
|
|
9192
9452
|
var parseAiTypeToken = (aiTypeRaw) => {
|
|
9193
9453
|
const raw = (aiTypeRaw ?? "").trim();
|
|
@@ -9497,6 +9757,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9497
9757
|
host;
|
|
9498
9758
|
username;
|
|
9499
9759
|
password;
|
|
9760
|
+
/**
|
|
9761
|
+
* Set to `true` after `close()` is called.
|
|
9762
|
+
* Once closed, the API instance should not be reused.
|
|
9763
|
+
*/
|
|
9764
|
+
_closed = false;
|
|
9500
9765
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9501
9766
|
// SOCKET POOL - Tag-based socket management
|
|
9502
9767
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -9526,10 +9791,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9526
9791
|
get client() {
|
|
9527
9792
|
const entry = this.socketPool.get("general");
|
|
9528
9793
|
if (!entry) {
|
|
9794
|
+
if (this._closed) {
|
|
9795
|
+
throw new Error(
|
|
9796
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
9797
|
+
);
|
|
9798
|
+
}
|
|
9529
9799
|
throw new Error("[ReolinkBaichuanApi] General socket not initialized");
|
|
9530
9800
|
}
|
|
9531
9801
|
return entry.client;
|
|
9532
9802
|
}
|
|
9803
|
+
/**
|
|
9804
|
+
* `true` after `close()` has been called. A closed API should not be reused;
|
|
9805
|
+
* the consumer should create a new instance.
|
|
9806
|
+
*/
|
|
9807
|
+
get isClosed() {
|
|
9808
|
+
return this._closed;
|
|
9809
|
+
}
|
|
9810
|
+
/**
|
|
9811
|
+
* `true` when the API is usable: not closed, general socket exists, socket
|
|
9812
|
+
* is connected and the client is logged in.
|
|
9813
|
+
*
|
|
9814
|
+
* This is the recommended way for consumers to check whether the API is
|
|
9815
|
+
* still valid before issuing commands, instead of directly accessing
|
|
9816
|
+
* `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
|
|
9817
|
+
* if the socket pool was already destroyed).
|
|
9818
|
+
*/
|
|
9819
|
+
get isReady() {
|
|
9820
|
+
if (this._closed) return false;
|
|
9821
|
+
const entry = this.socketPool.get("general");
|
|
9822
|
+
if (!entry) return false;
|
|
9823
|
+
try {
|
|
9824
|
+
return entry.client.isSocketConnected() && entry.client.loggedIn;
|
|
9825
|
+
} catch {
|
|
9826
|
+
return false;
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9829
|
+
/** Promise tracking an in-flight reconnection from `ensureConnected()`. */
|
|
9830
|
+
_ensureConnectedPromise;
|
|
9831
|
+
/**
|
|
9832
|
+
* Ensure the "general" socket is connected and logged in.
|
|
9833
|
+
* If the socket is disconnected or the pool entry was destroyed, a new
|
|
9834
|
+
* general socket is created, logged in, and all event/push/guard listeners
|
|
9835
|
+
* are re-attached automatically.
|
|
9836
|
+
*
|
|
9837
|
+
* This is a **no-op** when the API is already {@link isReady}.
|
|
9838
|
+
*
|
|
9839
|
+
* @throws If `close()` was called — the API is permanently closed and a new
|
|
9840
|
+
* instance must be created.
|
|
9841
|
+
*/
|
|
9842
|
+
async ensureConnected() {
|
|
9843
|
+
if (this._closed) {
|
|
9844
|
+
throw new Error(
|
|
9845
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
9846
|
+
);
|
|
9847
|
+
}
|
|
9848
|
+
if (this.isReady) return;
|
|
9849
|
+
if (this._ensureConnectedPromise) {
|
|
9850
|
+
return this._ensureConnectedPromise;
|
|
9851
|
+
}
|
|
9852
|
+
this._ensureConnectedPromise = this.reconnectGeneralSocket();
|
|
9853
|
+
try {
|
|
9854
|
+
await this._ensureConnectedPromise;
|
|
9855
|
+
} finally {
|
|
9856
|
+
this._ensureConnectedPromise = void 0;
|
|
9857
|
+
}
|
|
9858
|
+
}
|
|
9859
|
+
/**
|
|
9860
|
+
* Internal: destroy the current general socket (if any), create a new one,
|
|
9861
|
+
* login, and re-attach all listeners.
|
|
9862
|
+
*/
|
|
9863
|
+
async reconnectGeneralSocket() {
|
|
9864
|
+
const oldEntry = this.socketPool.get("general");
|
|
9865
|
+
if (oldEntry) {
|
|
9866
|
+
oldEntry.client.removeAllListeners();
|
|
9867
|
+
if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
|
|
9868
|
+
if (oldEntry.generalPermitRelease) {
|
|
9869
|
+
try {
|
|
9870
|
+
oldEntry.generalPermitRelease();
|
|
9871
|
+
} catch {
|
|
9872
|
+
}
|
|
9873
|
+
}
|
|
9874
|
+
this.socketPool.delete("general");
|
|
9875
|
+
try {
|
|
9876
|
+
await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
|
|
9877
|
+
} catch {
|
|
9878
|
+
}
|
|
9879
|
+
}
|
|
9880
|
+
const newClient = new BaichuanClient(this.clientOptions);
|
|
9881
|
+
this.socketPool.set("general", {
|
|
9882
|
+
client: newClient,
|
|
9883
|
+
refCount: 1,
|
|
9884
|
+
// general socket is always "in use"
|
|
9885
|
+
createdAt: Date.now(),
|
|
9886
|
+
lastUsedAt: Date.now(),
|
|
9887
|
+
idleCloseTimer: void 0,
|
|
9888
|
+
generalPermitRelease: void 0
|
|
9889
|
+
});
|
|
9890
|
+
this.setupGeneralClientListeners();
|
|
9891
|
+
await this.client.login();
|
|
9892
|
+
this.logger.log?.(
|
|
9893
|
+
"[ReolinkBaichuanApi] General socket reconnected successfully"
|
|
9894
|
+
);
|
|
9895
|
+
if (this.simpleEventListeners.size > 0) {
|
|
9896
|
+
this.simpleEventSubscribed = false;
|
|
9897
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
9898
|
+
this.simpleEventWatchdogLastRecoveryAt = 0;
|
|
9899
|
+
try {
|
|
9900
|
+
await this.ensureSimpleEventSubscribed();
|
|
9901
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
9902
|
+
this.logger.log?.(
|
|
9903
|
+
`[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
|
|
9904
|
+
);
|
|
9905
|
+
} catch (e) {
|
|
9906
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
9907
|
+
this.logger,
|
|
9908
|
+
`[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
|
|
9909
|
+
formatErrorForLog(e)
|
|
9910
|
+
);
|
|
9911
|
+
}
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
/**
|
|
9915
|
+
* Attach event, push, channelInfo, and guard listeners to the current
|
|
9916
|
+
* "general" client. Called from the constructor and from
|
|
9917
|
+
* {@link reconnectGeneralSocket}.
|
|
9918
|
+
*/
|
|
9919
|
+
setupGeneralClientListeners() {
|
|
9920
|
+
const client = this.client;
|
|
9921
|
+
client.on("event", (event) => {
|
|
9922
|
+
const mapped = mapToSimpleEvent(event);
|
|
9923
|
+
if (!mapped) return;
|
|
9924
|
+
this.dispatchSimpleEvent(mapped);
|
|
9925
|
+
});
|
|
9926
|
+
client.on("channelInfo", (xml) => {
|
|
9927
|
+
try {
|
|
9928
|
+
this.parseAndStoreChannelInfo(xml);
|
|
9929
|
+
} catch (e) {
|
|
9930
|
+
this.logger.warn?.(
|
|
9931
|
+
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
9932
|
+
formatErrorForLog(e)
|
|
9933
|
+
);
|
|
9934
|
+
}
|
|
9935
|
+
});
|
|
9936
|
+
client.on("push", (frame) => {
|
|
9937
|
+
const cmdId = frame.header.cmdId;
|
|
9938
|
+
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) {
|
|
9939
|
+
return;
|
|
9940
|
+
}
|
|
9941
|
+
try {
|
|
9942
|
+
if (frame.body.length === 0) return;
|
|
9943
|
+
const xml = client.tryDecryptXml(
|
|
9944
|
+
frame.body,
|
|
9945
|
+
frame.header.channelId,
|
|
9946
|
+
client.enc
|
|
9947
|
+
);
|
|
9948
|
+
if (!xml || !xml.startsWith("<?xml")) return;
|
|
9949
|
+
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
9950
|
+
} catch (e) {
|
|
9951
|
+
this.logger.debug?.(
|
|
9952
|
+
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
9953
|
+
formatErrorForLog(e)
|
|
9954
|
+
);
|
|
9955
|
+
}
|
|
9956
|
+
});
|
|
9957
|
+
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
9958
|
+
client.on("close", () => {
|
|
9959
|
+
try {
|
|
9960
|
+
void this.maybeRebootOnDisconnectStorm();
|
|
9961
|
+
} catch {
|
|
9962
|
+
}
|
|
9963
|
+
});
|
|
9964
|
+
}
|
|
9965
|
+
if (this.rebootAfterConsecutiveEconnreset > 0) {
|
|
9966
|
+
client.on("close", () => {
|
|
9967
|
+
try {
|
|
9968
|
+
void this.maybeRebootOnEconnresetStorm();
|
|
9969
|
+
} catch {
|
|
9970
|
+
}
|
|
9971
|
+
});
|
|
9972
|
+
}
|
|
9973
|
+
if (!this.sessionGuardIntervalTimer) {
|
|
9974
|
+
client.once("push", () => {
|
|
9975
|
+
void this.logActiveSessionsOnStartup();
|
|
9976
|
+
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
9977
|
+
void this.maybeRebootOnTooManySessions();
|
|
9978
|
+
}, 6e4);
|
|
9979
|
+
});
|
|
9980
|
+
}
|
|
9981
|
+
}
|
|
9533
9982
|
/**
|
|
9534
9983
|
* Cached camera UID. May be initially undefined if not provided in the constructor.
|
|
9535
9984
|
* Will be lazily populated on demand when needed (e.g. for recordings).
|
|
@@ -10104,6 +10553,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10104
10553
|
`[SocketPool] Closing existing socket for tag=${tag} (recreating)`
|
|
10105
10554
|
);
|
|
10106
10555
|
this.socketPool.delete(tag);
|
|
10556
|
+
if (existing.generalPermitRelease) {
|
|
10557
|
+
try {
|
|
10558
|
+
existing.generalPermitRelease();
|
|
10559
|
+
} catch {
|
|
10560
|
+
}
|
|
10561
|
+
existing.generalPermitRelease = void 0;
|
|
10562
|
+
}
|
|
10107
10563
|
try {
|
|
10108
10564
|
await existing.client.close({
|
|
10109
10565
|
reason: "socket pool recreation",
|
|
@@ -10122,7 +10578,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10122
10578
|
refCount: 0,
|
|
10123
10579
|
createdAt: now,
|
|
10124
10580
|
lastUsedAt: now,
|
|
10125
|
-
idleCloseTimer: void 0
|
|
10581
|
+
idleCloseTimer: void 0,
|
|
10582
|
+
generalPermitRelease: void 0
|
|
10126
10583
|
};
|
|
10127
10584
|
entry.pendingPromise = (async () => {
|
|
10128
10585
|
try {
|
|
@@ -10140,6 +10597,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10140
10597
|
entry.lastUsedAt = Date.now();
|
|
10141
10598
|
delete entry.pendingPromise;
|
|
10142
10599
|
log?.log?.(`[SocketPool] Socket connected for tag=${tag}`);
|
|
10600
|
+
if (tag !== "general") {
|
|
10601
|
+
try {
|
|
10602
|
+
const generalEntry = this.socketPool.get("general");
|
|
10603
|
+
if (generalEntry?.client) {
|
|
10604
|
+
entry.generalPermitRelease = generalEntry.client.acquirePermit(
|
|
10605
|
+
0,
|
|
10606
|
+
// indefinite — released when the streaming socket closes
|
|
10607
|
+
`streaming-peer:${tag}`
|
|
10608
|
+
);
|
|
10609
|
+
}
|
|
10610
|
+
} catch {
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10143
10613
|
void this.maybeRebootOnTooManySessions();
|
|
10144
10614
|
return newClient;
|
|
10145
10615
|
} catch (loginError) {
|
|
@@ -10208,6 +10678,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10208
10678
|
if (!current) return;
|
|
10209
10679
|
if (current.refCount > 0) return;
|
|
10210
10680
|
this.socketPool.delete(tag);
|
|
10681
|
+
if (current.generalPermitRelease) {
|
|
10682
|
+
try {
|
|
10683
|
+
current.generalPermitRelease();
|
|
10684
|
+
} catch {
|
|
10685
|
+
}
|
|
10686
|
+
current.generalPermitRelease = void 0;
|
|
10687
|
+
}
|
|
10211
10688
|
log?.log?.(`[SocketPool] Closing idle streaming socket for tag=${tag}`);
|
|
10212
10689
|
try {
|
|
10213
10690
|
await current.client.close({
|
|
@@ -10262,6 +10739,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10262
10739
|
clearTimeout(entry.idleCloseTimer);
|
|
10263
10740
|
entry.idleCloseTimer = void 0;
|
|
10264
10741
|
}
|
|
10742
|
+
if (entry.generalPermitRelease) {
|
|
10743
|
+
try {
|
|
10744
|
+
entry.generalPermitRelease();
|
|
10745
|
+
} catch {
|
|
10746
|
+
}
|
|
10747
|
+
entry.generalPermitRelease = void 0;
|
|
10748
|
+
}
|
|
10265
10749
|
log?.debug?.(`[SocketPool] Force-closing socket for tag=${tag}`);
|
|
10266
10750
|
this.socketPool.delete(tag);
|
|
10267
10751
|
try {
|
|
@@ -10287,6 +10771,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10287
10771
|
if (entry.idleCloseTimer) {
|
|
10288
10772
|
clearTimeout(entry.idleCloseTimer);
|
|
10289
10773
|
}
|
|
10774
|
+
if (entry.generalPermitRelease) {
|
|
10775
|
+
try {
|
|
10776
|
+
entry.generalPermitRelease();
|
|
10777
|
+
} catch {
|
|
10778
|
+
}
|
|
10779
|
+
entry.generalPermitRelease = void 0;
|
|
10780
|
+
}
|
|
10290
10781
|
this.logger?.debug?.(`[SocketPool] Cleanup: closing tag=${tag}`);
|
|
10291
10782
|
await entry.client.close({ reason: "API cleanup", skipLogout: true });
|
|
10292
10783
|
} catch {
|
|
@@ -10392,7 +10883,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10392
10883
|
password: opts.password,
|
|
10393
10884
|
...opts.logger ? { logger: opts.logger } : {},
|
|
10394
10885
|
...opts.debugOptions ? { debugOptions: opts.debugOptions } : {},
|
|
10395
|
-
...opts.uid ? { uid: opts.uid } : {}
|
|
10886
|
+
...opts.uid ? { uid: opts.uid } : {},
|
|
10887
|
+
...opts.transport ? { transport: opts.transport } : {},
|
|
10888
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
10889
|
+
...opts.udpDiscoveryMethod ? { udpDiscoveryMethod: opts.udpDiscoveryMethod } : {},
|
|
10890
|
+
...opts.idleDisconnect !== void 0 ? { idleDisconnect: opts.idleDisconnect } : {},
|
|
10891
|
+
...opts.idleDisconnectTimeoutMs !== void 0 ? { idleDisconnectTimeoutMs: opts.idleDisconnectTimeoutMs } : {},
|
|
10892
|
+
...opts.channel !== void 0 ? { channel: opts.channel } : {}
|
|
10396
10893
|
};
|
|
10397
10894
|
const generalClient = new BaichuanClient(opts);
|
|
10398
10895
|
this.socketPool.set("general", {
|
|
@@ -10401,7 +10898,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10401
10898
|
// Always keep general socket "in use"
|
|
10402
10899
|
createdAt: Date.now(),
|
|
10403
10900
|
lastUsedAt: Date.now(),
|
|
10404
|
-
idleCloseTimer: void 0
|
|
10901
|
+
idleCloseTimer: void 0,
|
|
10902
|
+
generalPermitRelease: void 0
|
|
10405
10903
|
});
|
|
10406
10904
|
this.host = opts.host;
|
|
10407
10905
|
this.username = opts.username;
|
|
@@ -10421,42 +10919,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10421
10919
|
logger: this.logger,
|
|
10422
10920
|
debugConfig: generalClient.getDebugConfig?.()
|
|
10423
10921
|
});
|
|
10424
|
-
this.client.on("event", (event) => {
|
|
10425
|
-
const mapped = mapToSimpleEvent(event);
|
|
10426
|
-
if (!mapped) return;
|
|
10427
|
-
this.dispatchSimpleEvent(mapped);
|
|
10428
|
-
});
|
|
10429
|
-
this.client.on("channelInfo", (xml) => {
|
|
10430
|
-
try {
|
|
10431
|
-
this.parseAndStoreChannelInfo(xml);
|
|
10432
|
-
} catch (e) {
|
|
10433
|
-
this.logger.warn?.(
|
|
10434
|
-
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
10435
|
-
formatErrorForLog(e)
|
|
10436
|
-
);
|
|
10437
|
-
}
|
|
10438
|
-
});
|
|
10439
|
-
this.client.on("push", (frame) => {
|
|
10440
|
-
const cmdId = frame.header.cmdId;
|
|
10441
|
-
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) {
|
|
10442
|
-
return;
|
|
10443
|
-
}
|
|
10444
|
-
try {
|
|
10445
|
-
if (frame.body.length === 0) return;
|
|
10446
|
-
const xml = this.client.tryDecryptXml(
|
|
10447
|
-
frame.body,
|
|
10448
|
-
frame.header.channelId,
|
|
10449
|
-
this.client.enc
|
|
10450
|
-
);
|
|
10451
|
-
if (!xml || !xml.startsWith("<?xml")) return;
|
|
10452
|
-
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
10453
|
-
} catch (e) {
|
|
10454
|
-
this.logger.debug?.(
|
|
10455
|
-
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
10456
|
-
formatErrorForLog(e)
|
|
10457
|
-
);
|
|
10458
|
-
}
|
|
10459
|
-
});
|
|
10460
10922
|
const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
|
|
10461
10923
|
if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
|
|
10462
10924
|
this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
|
|
@@ -10465,32 +10927,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10465
10927
|
if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
|
|
10466
10928
|
this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
|
|
10467
10929
|
}
|
|
10468
|
-
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
10469
|
-
this.client.on("close", () => {
|
|
10470
|
-
try {
|
|
10471
|
-
void this.maybeRebootOnDisconnectStorm();
|
|
10472
|
-
} catch {
|
|
10473
|
-
}
|
|
10474
|
-
});
|
|
10475
|
-
}
|
|
10476
10930
|
const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
|
|
10477
10931
|
if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
|
|
10478
10932
|
this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
|
|
10479
10933
|
}
|
|
10480
|
-
|
|
10481
|
-
this.client.on("close", () => {
|
|
10482
|
-
try {
|
|
10483
|
-
void this.maybeRebootOnEconnresetStorm();
|
|
10484
|
-
} catch {
|
|
10485
|
-
}
|
|
10486
|
-
});
|
|
10487
|
-
}
|
|
10488
|
-
this.client.once("push", () => {
|
|
10489
|
-
void this.logActiveSessionsOnStartup();
|
|
10490
|
-
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
10491
|
-
void this.maybeRebootOnTooManySessions();
|
|
10492
|
-
}, 6e4);
|
|
10493
|
-
});
|
|
10934
|
+
this.setupGeneralClientListeners();
|
|
10494
10935
|
}
|
|
10495
10936
|
/**
|
|
10496
10937
|
* CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
|
|
@@ -10904,7 +11345,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10904
11345
|
*/
|
|
10905
11346
|
async onSimpleEvent(callback) {
|
|
10906
11347
|
this.simpleEventListeners.add(callback);
|
|
10907
|
-
|
|
11348
|
+
this.logger.debug?.(
|
|
11349
|
+
`[ReolinkBaichuanApi] onSimpleEvent: registering listener (total=${this.simpleEventListeners.size})`
|
|
11350
|
+
);
|
|
11351
|
+
try {
|
|
11352
|
+
await this.ensureSimpleEventSubscribed();
|
|
11353
|
+
this.logger.debug?.(
|
|
11354
|
+
`[ReolinkBaichuanApi] onSimpleEvent: initial subscribe succeeded, simpleEventSubscribed=${this.simpleEventSubscribed}`
|
|
11355
|
+
);
|
|
11356
|
+
} catch (e) {
|
|
11357
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
11358
|
+
this.logger,
|
|
11359
|
+
`[ReolinkBaichuanApi] onSimpleEvent: initial subscribe failed, simpleEventSubscribed=${this.simpleEventSubscribed}, watchdog will retry`,
|
|
11360
|
+
formatErrorForLog(e)
|
|
11361
|
+
);
|
|
11362
|
+
}
|
|
10908
11363
|
this.simpleEventLastReceivedAt = Date.now();
|
|
10909
11364
|
this.startSimpleEventResubscribeTimer();
|
|
10910
11365
|
this.startSimpleEventWatchdog();
|
|
@@ -10925,11 +11380,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10925
11380
|
this.stopUdpSleepInference();
|
|
10926
11381
|
await this.ensureSimpleEventUnsubscribed();
|
|
10927
11382
|
} else {
|
|
10928
|
-
const
|
|
10929
|
-
if (
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
11383
|
+
const generalEntry = this.socketPool.get("general");
|
|
11384
|
+
if (generalEntry) {
|
|
11385
|
+
const isUdp = generalEntry.client.getTransport?.() === "udp";
|
|
11386
|
+
if (isUdp) {
|
|
11387
|
+
this.startUdpSleepInference();
|
|
11388
|
+
} else if (generalEntry.client.isStatePollingEnabled?.()) {
|
|
11389
|
+
this.startStatePolling();
|
|
11390
|
+
}
|
|
10933
11391
|
}
|
|
10934
11392
|
}
|
|
10935
11393
|
}
|
|
@@ -10971,8 +11429,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10971
11429
|
}
|
|
10972
11430
|
async simpleEventWatchdogTick() {
|
|
10973
11431
|
if (this.simpleEventListeners.size === 0) return;
|
|
10974
|
-
|
|
11432
|
+
const generalEntry = this.socketPool.get("general");
|
|
11433
|
+
if (!generalEntry) return;
|
|
11434
|
+
if (!generalEntry.client.isSocketConnected?.() || !generalEntry.client.loggedIn) {
|
|
11435
|
+
this.logger.debug?.(
|
|
11436
|
+
`[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
|
|
11437
|
+
);
|
|
11438
|
+
return;
|
|
11439
|
+
}
|
|
10975
11440
|
const now = Date.now();
|
|
11441
|
+
const sinceLastEvent = this.simpleEventLastReceivedAt > 0 ? now - this.simpleEventLastReceivedAt : -1;
|
|
11442
|
+
this.logger.debug?.(
|
|
11443
|
+
`[ReolinkBaichuanApi] event watchdog tick: subscribed=${this.simpleEventSubscribed} clientSubscribed=${generalEntry.client.subscribed} lastEventAgoMs=${sinceLastEvent} recoveryAttempts=${this.simpleEventWatchdogRecoveryAttempts} listeners=${this.simpleEventListeners.size}`
|
|
11444
|
+
);
|
|
10976
11445
|
if (this.simpleEventSubscribed && this.simpleEventLastReceivedAt > 0) {
|
|
10977
11446
|
const silence = now - this.simpleEventLastReceivedAt;
|
|
10978
11447
|
if (silence < this.simpleEventWatchdogSilenceThresholdMs) return;
|
|
@@ -10983,7 +11452,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10983
11452
|
);
|
|
10984
11453
|
try {
|
|
10985
11454
|
this.simpleEventSubscribed = false;
|
|
10986
|
-
|
|
11455
|
+
generalEntry.client.subscribed = false;
|
|
10987
11456
|
await this.ensureSimpleEventSubscribed();
|
|
10988
11457
|
this.simpleEventLastReceivedAt = Date.now();
|
|
10989
11458
|
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
@@ -11001,6 +11470,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11001
11470
|
return;
|
|
11002
11471
|
}
|
|
11003
11472
|
if (!this.simpleEventSubscribed) {
|
|
11473
|
+
if (this.simpleEventLastReceivedAt > 0) {
|
|
11474
|
+
const sinceLastEvent2 = now - this.simpleEventLastReceivedAt;
|
|
11475
|
+
if (sinceLastEvent2 < this.simpleEventWatchdogSilenceThresholdMs) {
|
|
11476
|
+
this.simpleEventSubscribed = true;
|
|
11477
|
+
this.logger.debug?.(
|
|
11478
|
+
`[ReolinkBaichuanApi] event watchdog: events flowing (lastEventAgo=${Math.round(sinceLastEvent2 / 1e3)}s) despite simpleEventSubscribed=false, marking subscription as active (recoveryAttempts=${this.simpleEventWatchdogRecoveryAttempts})`
|
|
11479
|
+
);
|
|
11480
|
+
if (this.simpleEventWatchdogRecoveryAttempts > 0) {
|
|
11481
|
+
(this.logger.info ?? this.logger.log).call(
|
|
11482
|
+
this.logger,
|
|
11483
|
+
`[ReolinkBaichuanApi] event watchdog: events flowing despite failed subscribe, marking subscription active`
|
|
11484
|
+
);
|
|
11485
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
11486
|
+
}
|
|
11487
|
+
return;
|
|
11488
|
+
} else {
|
|
11489
|
+
this.logger.debug?.(
|
|
11490
|
+
`[ReolinkBaichuanApi] event watchdog: events stale (lastEventAgo=${Math.round(sinceLastEvent2 / 1e3)}s, threshold=${Math.round(this.simpleEventWatchdogSilenceThresholdMs / 1e3)}s), proceeding with recovery`
|
|
11491
|
+
);
|
|
11492
|
+
}
|
|
11493
|
+
} else {
|
|
11494
|
+
this.logger.debug?.(
|
|
11495
|
+
`[ReolinkBaichuanApi] event watchdog: no events ever received (simpleEventLastReceivedAt=0), proceeding with recovery`
|
|
11496
|
+
);
|
|
11497
|
+
}
|
|
11004
11498
|
const backoffMs = Math.min(
|
|
11005
11499
|
3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
|
|
11006
11500
|
this.simpleEventWatchdogSilenceThresholdMs
|
|
@@ -11064,20 +11558,51 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11064
11558
|
return await this.simpleEventResubscribeInFlight;
|
|
11065
11559
|
}
|
|
11066
11560
|
async ensureSimpleEventSubscribed() {
|
|
11067
|
-
if (this.simpleEventListeners.size === 0)
|
|
11068
|
-
|
|
11069
|
-
|
|
11561
|
+
if (this.simpleEventListeners.size === 0) {
|
|
11562
|
+
this.logger.debug?.(
|
|
11563
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: no listeners, skipping`
|
|
11564
|
+
);
|
|
11565
|
+
return;
|
|
11566
|
+
}
|
|
11567
|
+
if (this.simpleEventSubscribed) {
|
|
11568
|
+
this.logger.debug?.(
|
|
11569
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: already subscribed, skipping`
|
|
11570
|
+
);
|
|
11571
|
+
return;
|
|
11572
|
+
}
|
|
11573
|
+
if (this.simpleEventSubscribeInFlight) {
|
|
11574
|
+
this.logger.debug?.(
|
|
11575
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: subscribe already in-flight, awaiting`
|
|
11576
|
+
);
|
|
11070
11577
|
return await this.simpleEventSubscribeInFlight;
|
|
11578
|
+
}
|
|
11579
|
+
this.logger.debug?.(
|
|
11580
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: starting subscribe (clientSubscribed=${this.socketPool.get("general")?.client.subscribed})`
|
|
11581
|
+
);
|
|
11071
11582
|
this.simpleEventSubscribeInFlight = (async () => {
|
|
11072
|
-
|
|
11583
|
+
const entry = this.socketPool.get("general");
|
|
11584
|
+
if (!entry) {
|
|
11585
|
+
this.logger.debug?.(
|
|
11586
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: no general socket, bailing out`
|
|
11587
|
+
);
|
|
11588
|
+
return;
|
|
11589
|
+
}
|
|
11590
|
+
if (!entry.client.subscribed) {
|
|
11591
|
+
this.logger.debug?.(
|
|
11592
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: client.subscribed=false, calling subscribeEvents()`
|
|
11593
|
+
);
|
|
11073
11594
|
await this.subscribeEvents();
|
|
11595
|
+
} else {
|
|
11596
|
+
this.logger.debug?.(
|
|
11597
|
+
`[ReolinkBaichuanApi] ensureSimpleEventSubscribed: client already subscribed, skipping subscribeEvents()`
|
|
11598
|
+
);
|
|
11074
11599
|
}
|
|
11075
11600
|
this.simpleEventSubscribed = true;
|
|
11076
|
-
const isUdp =
|
|
11601
|
+
const isUdp = entry.client.getTransport?.() === "udp";
|
|
11077
11602
|
if (isUdp) {
|
|
11078
11603
|
this.startUdpSleepInference();
|
|
11079
|
-
} else if (
|
|
11080
|
-
const channel =
|
|
11604
|
+
} else if (entry.client.isStatePollingEnabled?.()) {
|
|
11605
|
+
const channel = entry.client.getConfiguredChannel?.() ?? 0;
|
|
11081
11606
|
await this.checkAndDispatchCurrentState(channel);
|
|
11082
11607
|
this.startStatePolling();
|
|
11083
11608
|
}
|
|
@@ -11087,7 +11612,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11087
11612
|
return await this.simpleEventSubscribeInFlight;
|
|
11088
11613
|
}
|
|
11089
11614
|
async ensureSimpleEventUnsubscribed() {
|
|
11090
|
-
|
|
11615
|
+
const generalEntry = this.socketPool.get("general");
|
|
11616
|
+
if (!generalEntry) {
|
|
11617
|
+
this.simpleEventSubscribed = false;
|
|
11618
|
+
this.stopSimpleEventResubscribeTimer();
|
|
11619
|
+
this.stopStatePolling();
|
|
11620
|
+
this.stopUdpSleepInference();
|
|
11621
|
+
return;
|
|
11622
|
+
}
|
|
11623
|
+
if (!this.simpleEventSubscribed && !generalEntry.client.subscribed) return;
|
|
11091
11624
|
if (this.simpleEventUnsubscribeInFlight)
|
|
11092
11625
|
return await this.simpleEventUnsubscribeInFlight;
|
|
11093
11626
|
if (this.simpleEventSubscribeInFlight) {
|
|
@@ -11229,6 +11762,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11229
11762
|
);
|
|
11230
11763
|
}
|
|
11231
11764
|
async close(options) {
|
|
11765
|
+
if (this._closed) return;
|
|
11766
|
+
this._closed = true;
|
|
11232
11767
|
if (this.sessionGuardIntervalTimer) {
|
|
11233
11768
|
clearInterval(this.sessionGuardIntervalTimer);
|
|
11234
11769
|
this.sessionGuardIntervalTimer = void 0;
|
|
@@ -11291,7 +11826,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11291
11826
|
}
|
|
11292
11827
|
async handleSendXml400(params, frame, retry) {
|
|
11293
11828
|
const emptyBody = frame.body.length === 0;
|
|
11294
|
-
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes:
|
|
11829
|
+
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
|
|
11295
11830
|
if (this.isSendXmlFailFast400(params, frame.body.length)) {
|
|
11296
11831
|
throw new Error(emptyBody400Msg);
|
|
11297
11832
|
}
|
|
@@ -11807,11 +12342,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11807
12342
|
* Minimal per-channel inventory for NVR-connected devices.
|
|
11808
12343
|
*
|
|
11809
12344
|
* Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
|
|
12345
|
+
*
|
|
12346
|
+
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
12347
|
+
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
12348
|
+
* no dependency on async push messages. Recommended for first-call discovery.
|
|
12349
|
+
* - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
|
|
12350
|
+
* info after login + event subscription. This push is *asynchronous*: if it has not
|
|
12351
|
+
* arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
|
|
12352
|
+
* with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
|
|
11810
12353
|
*/
|
|
11811
12354
|
async getNvrChannelsSummary(options) {
|
|
11812
|
-
const source = options?.source ?? "
|
|
11813
|
-
|
|
11814
|
-
const
|
|
12355
|
+
const source = options?.source ?? "cgi";
|
|
12356
|
+
let channels;
|
|
12357
|
+
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
12358
|
+
if (options?.channels?.length) {
|
|
12359
|
+
channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
12360
|
+
} else if (source === "cgi") {
|
|
12361
|
+
try {
|
|
12362
|
+
const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
|
|
12363
|
+
const status = channelsResponse?.[0]?.value?.status ?? [];
|
|
12364
|
+
for (const s of status) {
|
|
12365
|
+
const ch = Number(s?.channel);
|
|
12366
|
+
if (!Number.isFinite(ch)) continue;
|
|
12367
|
+
cgiStatusByChannel.set(ch, {
|
|
12368
|
+
...s.name != null ? { name: s.name } : {},
|
|
12369
|
+
...s.uid != null ? { uid: s.uid } : {},
|
|
12370
|
+
sleeping: s.sleep === 1
|
|
12371
|
+
});
|
|
12372
|
+
}
|
|
12373
|
+
channels = cgiChannels;
|
|
12374
|
+
this.logger.debug?.(
|
|
12375
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
12376
|
+
);
|
|
12377
|
+
} catch (e) {
|
|
12378
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
12379
|
+
this.logger.warn?.(
|
|
12380
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
|
|
12381
|
+
);
|
|
12382
|
+
channels = [];
|
|
12383
|
+
}
|
|
12384
|
+
} else {
|
|
12385
|
+
const pushInfo2 = this.getChannelInfoFromPushCache();
|
|
12386
|
+
channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
12387
|
+
}
|
|
12388
|
+
channels = channels.sort((a, b) => a - b);
|
|
11815
12389
|
const support = await this.getSupportInfo().catch(() => {
|
|
11816
12390
|
this.logger.error?.(
|
|
11817
12391
|
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
@@ -11841,7 +12415,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11841
12415
|
);
|
|
11842
12416
|
}
|
|
11843
12417
|
}
|
|
11844
|
-
const cacheKey =
|
|
12418
|
+
const cacheKey = `${source}:${channels.join(",")}`;
|
|
11845
12419
|
const cached = this.nvrChannelsSummaryCache.get(cacheKey);
|
|
11846
12420
|
if (cached) {
|
|
11847
12421
|
return {
|
|
@@ -11862,8 +12436,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11862
12436
|
} catch {
|
|
11863
12437
|
}
|
|
11864
12438
|
}
|
|
12439
|
+
const pushInfo = this.getChannelInfoFromPushCache();
|
|
11865
12440
|
const devices = channels.map((channel) => {
|
|
11866
|
-
const
|
|
12441
|
+
const pushCached = pushInfo.get(channel);
|
|
12442
|
+
const cgiStatus = cgiStatusByChannel.get(channel);
|
|
11867
12443
|
const info = infoPerChannel.get(channel);
|
|
11868
12444
|
const networkInfo = networkInfoPerChannel.get(channel);
|
|
11869
12445
|
const isBattery = isBatteryByChannel.get(channel) ?? false;
|
|
@@ -11871,6 +12447,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11871
12447
|
const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
|
|
11872
12448
|
const normalizedModel = model ? model.trim() : void 0;
|
|
11873
12449
|
const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
12450
|
+
const name = pushCached?.name || cgiStatus?.name || "";
|
|
12451
|
+
const uid = pushCached?.uid || cgiStatus?.uid || "";
|
|
12452
|
+
const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
|
|
11874
12453
|
return {
|
|
11875
12454
|
channel,
|
|
11876
12455
|
isBattery,
|
|
@@ -11880,19 +12459,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11880
12459
|
...networkInfo?.ip ? { ip: networkInfo.ip } : {},
|
|
11881
12460
|
...networkInfo?.mac ? { mac: networkInfo.mac } : {},
|
|
11882
12461
|
...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
|
|
11883
|
-
...
|
|
11884
|
-
...
|
|
11885
|
-
...
|
|
11886
|
-
...typeof
|
|
11887
|
-
...
|
|
11888
|
-
...
|
|
11889
|
-
...
|
|
11890
|
-
...typeof
|
|
11891
|
-
...typeof
|
|
11892
|
-
...typeof
|
|
11893
|
-
...typeof
|
|
11894
|
-
...
|
|
11895
|
-
...typeof
|
|
12462
|
+
...name ? { name } : {},
|
|
12463
|
+
...uid ? { uid } : {},
|
|
12464
|
+
...pushCached?.state ? { state: pushCached.state } : {},
|
|
12465
|
+
...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
|
|
12466
|
+
...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
|
|
12467
|
+
...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
|
|
12468
|
+
...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
|
|
12469
|
+
...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
|
|
12470
|
+
...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
|
|
12471
|
+
...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
|
|
12472
|
+
...typeof sleeping === "boolean" ? { sleeping } : {},
|
|
12473
|
+
...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
|
|
12474
|
+
...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
|
|
11896
12475
|
};
|
|
11897
12476
|
});
|
|
11898
12477
|
const result = { channels, devices };
|
|
@@ -13072,6 +13651,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
13072
13651
|
this._processVideoclipThumbnailQueue();
|
|
13073
13652
|
}
|
|
13074
13653
|
}
|
|
13654
|
+
if (this.videoclipThumbnailQueue.length >= 50) {
|
|
13655
|
+
throw new Error(
|
|
13656
|
+
`Thumbnail queue full (${this.videoclipThumbnailQueue.length}/50) \u2013 request rejected to protect camera stability`
|
|
13657
|
+
);
|
|
13658
|
+
}
|
|
13075
13659
|
return new Promise((resolve, reject) => {
|
|
13076
13660
|
this.videoclipThumbnailQueue.push({ params, resolve, reject });
|
|
13077
13661
|
});
|
|
@@ -13172,10 +13756,7 @@ ${xml}`);
|
|
|
13172
13756
|
messageClass: BC_CLASS_MODERN_24,
|
|
13173
13757
|
streamType: 0,
|
|
13174
13758
|
payloadXml: xml,
|
|
13175
|
-
timeoutMs
|
|
13176
|
-
// Retry parameters - camera often rejects first few requests
|
|
13177
|
-
maxRetries: 8,
|
|
13178
|
-
retryDelayMs: 1500
|
|
13759
|
+
timeoutMs
|
|
13179
13760
|
});
|
|
13180
13761
|
trace(`CoverPreview succeeded`);
|
|
13181
13762
|
} catch (e) {
|
|
@@ -14568,8 +15149,10 @@ ${stderr}`)
|
|
|
14568
15149
|
* Unsubscribe from events.
|
|
14569
15150
|
*/
|
|
14570
15151
|
async unsubscribeEvents() {
|
|
14571
|
-
this.
|
|
14572
|
-
|
|
15152
|
+
const generalEntry = this.socketPool.get("general");
|
|
15153
|
+
if (!generalEntry) return;
|
|
15154
|
+
generalEntry.client.subscribed = false;
|
|
15155
|
+
generalEntry.client.refreshKeepAlive?.();
|
|
14573
15156
|
}
|
|
14574
15157
|
/**
|
|
14575
15158
|
* Check current motion and AI state and dispatch events if state changed.
|
|
@@ -16156,13 +16739,12 @@ ${xml}`
|
|
|
16156
16739
|
]);
|
|
16157
16740
|
const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
|
|
16158
16741
|
const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
|
|
16159
|
-
const supportItem =
|
|
16160
|
-
const capabilities =
|
|
16161
|
-
ch,
|
|
16162
|
-
|
|
16163
|
-
|
|
16164
|
-
|
|
16165
|
-
);
|
|
16742
|
+
const supportItem = getSupportItemForChannel(support, ch);
|
|
16743
|
+
const capabilities = computeDeviceCapabilities({
|
|
16744
|
+
channel: ch,
|
|
16745
|
+
...support != null && { support },
|
|
16746
|
+
...abilities != null && { abilities }
|
|
16747
|
+
});
|
|
16166
16748
|
const item = supportItem;
|
|
16167
16749
|
const lightType = item?.lightType;
|
|
16168
16750
|
const ledCtrl = item?.ledCtrl;
|
|
@@ -16178,6 +16760,25 @@ ${xml}`
|
|
|
16178
16760
|
});
|
|
16179
16761
|
capabilities.hasFloodlight = probed;
|
|
16180
16762
|
}
|
|
16763
|
+
let dingDongListIds;
|
|
16764
|
+
let dingDongCfgIds;
|
|
16765
|
+
let wirelessChimeError;
|
|
16766
|
+
if (capabilities.hasWirelessChime) {
|
|
16767
|
+
try {
|
|
16768
|
+
const list = await this.getDingDongList(ch);
|
|
16769
|
+
dingDongListIds = list.map((d) => d.id);
|
|
16770
|
+
const first = list[0];
|
|
16771
|
+
const fromList = first !== void 0 && first.id >= 0;
|
|
16772
|
+
if (!fromList) {
|
|
16773
|
+
const configs = await this.getDingDongCfg(ch);
|
|
16774
|
+
dingDongCfgIds = configs.map((c) => c.id);
|
|
16775
|
+
capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
|
|
16776
|
+
}
|
|
16777
|
+
} catch (e) {
|
|
16778
|
+
capabilities.hasWirelessChime = false;
|
|
16779
|
+
wirelessChimeError = e instanceof Error ? e.message : String(e);
|
|
16780
|
+
}
|
|
16781
|
+
}
|
|
16181
16782
|
const features = this.parseFeaturesFromSupport(support);
|
|
16182
16783
|
const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
|
|
16183
16784
|
const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
|
|
@@ -16214,7 +16815,10 @@ ${xml}`
|
|
|
16214
16815
|
...abilities && {
|
|
16215
16816
|
abilityMergedKeyCount: Object.keys(abilities).length
|
|
16216
16817
|
},
|
|
16217
|
-
...support?.items && { supportItemCount: support.items.length }
|
|
16818
|
+
...support?.items && { supportItemCount: support.items.length },
|
|
16819
|
+
...dingDongListIds !== void 0 && { dingDongListIds },
|
|
16820
|
+
...dingDongCfgIds !== void 0 && { dingDongCfgIds },
|
|
16821
|
+
...wirelessChimeError !== void 0 && { wirelessChimeError }
|
|
16218
16822
|
};
|
|
16219
16823
|
const result = {
|
|
16220
16824
|
capabilities,
|
|
@@ -16241,90 +16845,6 @@ ${xml}`
|
|
|
16241
16845
|
this.deviceCapabilitiesCache.clear();
|
|
16242
16846
|
}
|
|
16243
16847
|
}
|
|
16244
|
-
/**
|
|
16245
|
-
* Pick the best SupportItem for a channel.
|
|
16246
|
-
* Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
|
|
16247
|
-
*/
|
|
16248
|
-
pickBestSupportItem(support, channel) {
|
|
16249
|
-
if (!support?.items?.length) return void 0;
|
|
16250
|
-
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
16251
|
-
if (!candidates.length) return void 0;
|
|
16252
|
-
const score = (item) => {
|
|
16253
|
-
const anyItem = item;
|
|
16254
|
-
let result = 0;
|
|
16255
|
-
if (anyItem.name == null) result += 100;
|
|
16256
|
-
const capabilityKeys = [
|
|
16257
|
-
"ptzType",
|
|
16258
|
-
"ptzControl",
|
|
16259
|
-
"ptzPreset",
|
|
16260
|
-
"ledCtrl",
|
|
16261
|
-
"lightType",
|
|
16262
|
-
"battery",
|
|
16263
|
-
"audioVersion",
|
|
16264
|
-
"motion",
|
|
16265
|
-
"encCtrl",
|
|
16266
|
-
"newIspCfg",
|
|
16267
|
-
"remoteAbility",
|
|
16268
|
-
"aitype",
|
|
16269
|
-
"videoClip",
|
|
16270
|
-
"snap"
|
|
16271
|
-
];
|
|
16272
|
-
for (const k of capabilityKeys) {
|
|
16273
|
-
if (anyItem[k] !== void 0) result += 3;
|
|
16274
|
-
}
|
|
16275
|
-
return result;
|
|
16276
|
-
};
|
|
16277
|
-
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
16278
|
-
}
|
|
16279
|
-
/**
|
|
16280
|
-
* Parse device capabilities from SupportInfo.
|
|
16281
|
-
* Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
|
|
16282
|
-
*/
|
|
16283
|
-
parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
|
|
16284
|
-
const truthy = (v) => {
|
|
16285
|
-
if (typeof v === "number") return v > 0;
|
|
16286
|
-
if (typeof v === "string") {
|
|
16287
|
-
const n = Number(v);
|
|
16288
|
-
return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
|
|
16289
|
-
}
|
|
16290
|
-
return Boolean(v);
|
|
16291
|
-
};
|
|
16292
|
-
const item = supportItem;
|
|
16293
|
-
const ptzMode = support?.ptzMode?.toLowerCase();
|
|
16294
|
-
const ptzType = item ? truthy(item.ptzType) : false;
|
|
16295
|
-
const ptzControl = item ? truthy(item.ptzControl) : false;
|
|
16296
|
-
const hasPtzFromItem = ptzType || ptzControl;
|
|
16297
|
-
const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
|
|
16298
|
-
const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
|
|
16299
|
-
const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
|
|
16300
|
-
const hasPresets = item ? truthy(item.ptzPreset) : false;
|
|
16301
|
-
const hasBattery = item ? truthy(item.battery) : false;
|
|
16302
|
-
const hasSiren = item ? truthy(item.audioVersion) : false;
|
|
16303
|
-
const lightType = item?.lightType;
|
|
16304
|
-
const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
|
|
16305
|
-
const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
|
|
16306
|
-
const isDoorbell = item ? truthy(item.doorbellVersion) : false;
|
|
16307
|
-
const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
|
|
16308
|
-
return {
|
|
16309
|
-
channel,
|
|
16310
|
-
...ptzMode && { ptzMode },
|
|
16311
|
-
hasPan: hasPanTilt,
|
|
16312
|
-
hasTilt: hasPanTilt,
|
|
16313
|
-
hasZoom,
|
|
16314
|
-
hasPresets,
|
|
16315
|
-
hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
|
|
16316
|
-
hasBattery,
|
|
16317
|
-
hasIntercom,
|
|
16318
|
-
hasSiren,
|
|
16319
|
-
hasFloodlight,
|
|
16320
|
-
hasPir,
|
|
16321
|
-
isDoorbell,
|
|
16322
|
-
// Autotracking: explicit flags only (autoPt or smartAI)
|
|
16323
|
-
// Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
|
|
16324
|
-
// on cameras that have PTZ and AI detection but NOT autotracking capability.
|
|
16325
|
-
hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
|
|
16326
|
-
};
|
|
16327
|
-
}
|
|
16328
16848
|
/**
|
|
16329
16849
|
* Parse support features from SupportInfo.
|
|
16330
16850
|
*/
|
|
@@ -17093,7 +17613,7 @@ ${xml}`
|
|
|
17093
17613
|
* @returns Test results for all stream types and profiles
|
|
17094
17614
|
*/
|
|
17095
17615
|
async testChannelStreams(channel, logger) {
|
|
17096
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
17616
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-FNLGCOVA.js");
|
|
17097
17617
|
return await testChannelStreams({
|
|
17098
17618
|
api: this,
|
|
17099
17619
|
channel: this.normalizeChannel(channel),
|
|
@@ -17109,7 +17629,7 @@ ${xml}`
|
|
|
17109
17629
|
* @returns Complete diagnostics for all channels and streams
|
|
17110
17630
|
*/
|
|
17111
17631
|
async collectMultifocalDiagnostics(logger) {
|
|
17112
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
17632
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-FNLGCOVA.js");
|
|
17113
17633
|
return await collectMultifocalDiagnostics({
|
|
17114
17634
|
api: this,
|
|
17115
17635
|
logger
|
|
@@ -19197,6 +19717,216 @@ ${scheduleItems}
|
|
|
19197
19717
|
const channel = 0;
|
|
19198
19718
|
return await this.getSnapshot(channel);
|
|
19199
19719
|
}
|
|
19720
|
+
// --------------------
|
|
19721
|
+
// Chime / DingDong APIs
|
|
19722
|
+
// --------------------
|
|
19723
|
+
/**
|
|
19724
|
+
* Get the list of paired wireless chime devices.
|
|
19725
|
+
* cmd_id: 484 (GetDingDongList)
|
|
19726
|
+
*
|
|
19727
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19728
|
+
* @returns Array of paired chime devices
|
|
19729
|
+
*/
|
|
19730
|
+
async getDingDongList(channel) {
|
|
19731
|
+
const ch = this.normalizeChannel(channel);
|
|
19732
|
+
const xml = await this.sendXml({
|
|
19733
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
|
|
19734
|
+
channel: ch
|
|
19735
|
+
});
|
|
19736
|
+
return parseDingDongListFromXml(xml);
|
|
19737
|
+
}
|
|
19738
|
+
/**
|
|
19739
|
+
* Get parameters (name, volume, LED state) for a specific wireless chime.
|
|
19740
|
+
* cmd_id: 485 (DingDongOpt, option getParam)
|
|
19741
|
+
*
|
|
19742
|
+
* @param chimeId - The chime device ID
|
|
19743
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19744
|
+
* @returns Chime parameters
|
|
19745
|
+
*/
|
|
19746
|
+
async getDingDongParams(chimeId, channel) {
|
|
19747
|
+
const ch = this.normalizeChannel(channel);
|
|
19748
|
+
const payloadXml = buildDingDongGetParamsXml(chimeId);
|
|
19749
|
+
const xml = await this.sendXml({
|
|
19750
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
19751
|
+
channel: ch,
|
|
19752
|
+
payloadXml
|
|
19753
|
+
});
|
|
19754
|
+
return parseDingDongParamsFromXml(xml);
|
|
19755
|
+
}
|
|
19756
|
+
/**
|
|
19757
|
+
* Set parameters (name, volume, LED state) for a specific wireless chime.
|
|
19758
|
+
* cmd_id: 485 (DingDongOpt, option setParam)
|
|
19759
|
+
*
|
|
19760
|
+
* @param chimeId - The chime device ID
|
|
19761
|
+
* @param params - Parameters to set (volLevel, ledState, name)
|
|
19762
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19763
|
+
*/
|
|
19764
|
+
async setDingDongParams(chimeId, params, channel) {
|
|
19765
|
+
const ch = this.normalizeChannel(channel);
|
|
19766
|
+
const payloadXml = buildDingDongSetParamsXml(chimeId, params);
|
|
19767
|
+
await this.sendXml({
|
|
19768
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
19769
|
+
channel: ch,
|
|
19770
|
+
payloadXml
|
|
19771
|
+
});
|
|
19772
|
+
}
|
|
19773
|
+
/**
|
|
19774
|
+
* Trigger a wireless chime to ring with a specific ringtone.
|
|
19775
|
+
* cmd_id: 485 (DingDongOpt, option ringWithMusic)
|
|
19776
|
+
*
|
|
19777
|
+
* @param chimeId - The chime device ID
|
|
19778
|
+
* @param musicId - The ringtone/music ID to play
|
|
19779
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19780
|
+
*/
|
|
19781
|
+
async ringDingDong(chimeId, musicId, channel) {
|
|
19782
|
+
const ch = this.normalizeChannel(channel);
|
|
19783
|
+
const payloadXml = buildDingDongRingXml(chimeId, musicId);
|
|
19784
|
+
await this.sendXml({
|
|
19785
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
19786
|
+
channel: ch,
|
|
19787
|
+
payloadXml
|
|
19788
|
+
});
|
|
19789
|
+
}
|
|
19790
|
+
/**
|
|
19791
|
+
* Get the per-event alarm configuration for paired wireless chimes.
|
|
19792
|
+
* cmd_id: 486 (GetDingDongCfg)
|
|
19793
|
+
*
|
|
19794
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19795
|
+
* @returns Array of chime configurations (one per paired chime)
|
|
19796
|
+
*/
|
|
19797
|
+
async getDingDongCfg(channel) {
|
|
19798
|
+
const ch = this.normalizeChannel(channel);
|
|
19799
|
+
const xml = await this.sendXml({
|
|
19800
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
|
|
19801
|
+
channel: ch
|
|
19802
|
+
});
|
|
19803
|
+
return parseDingDongCfgFromXml(xml);
|
|
19804
|
+
}
|
|
19805
|
+
/**
|
|
19806
|
+
* Set the per-event alarm configuration for a specific wireless chime.
|
|
19807
|
+
* cmd_id: 487 (SetDingDongCfg)
|
|
19808
|
+
*
|
|
19809
|
+
* @param chimeId - The chime ring/device ID
|
|
19810
|
+
* @param eventType - Event type string (e.g. "doorbell", "package", "people")
|
|
19811
|
+
* @param state - 0 = disabled, 1 = enabled
|
|
19812
|
+
* @param musicId - Ringtone ID to use for this event type
|
|
19813
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19814
|
+
*/
|
|
19815
|
+
async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
|
|
19816
|
+
const ch = this.normalizeChannel(channel);
|
|
19817
|
+
const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
|
|
19818
|
+
await this.sendXml({
|
|
19819
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
|
|
19820
|
+
channel: ch,
|
|
19821
|
+
payloadXml
|
|
19822
|
+
});
|
|
19823
|
+
}
|
|
19824
|
+
/** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
|
|
19825
|
+
_hardwiredChimeCache = /* @__PURE__ */ new Map();
|
|
19826
|
+
/**
|
|
19827
|
+
* Get the hardwired (wired-in) chime state.
|
|
19828
|
+
* cmd_id: 483 (GetDingDongCtrl)
|
|
19829
|
+
*
|
|
19830
|
+
* Note: calling this may briefly trigger the physical chime to rattle.
|
|
19831
|
+
*
|
|
19832
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19833
|
+
* @returns Hardwired chime state (type, enabled, time)
|
|
19834
|
+
*/
|
|
19835
|
+
async getHardwiredChime(channel) {
|
|
19836
|
+
const ch = this.normalizeChannel(channel);
|
|
19837
|
+
const payloadXml = buildGetDingDongCtrlXml();
|
|
19838
|
+
const xml = await this.sendXml({
|
|
19839
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
19840
|
+
channel: ch,
|
|
19841
|
+
payloadXml
|
|
19842
|
+
});
|
|
19843
|
+
const state = parseHardwiredChimeFromXml(xml);
|
|
19844
|
+
this._hardwiredChimeCache.set(ch, state);
|
|
19845
|
+
return state;
|
|
19846
|
+
}
|
|
19847
|
+
/**
|
|
19848
|
+
* Set the hardwired (wired-in) chime state.
|
|
19849
|
+
* cmd_id: 483 (SetDingDongCtrl)
|
|
19850
|
+
*
|
|
19851
|
+
* Uses the cached state from a previous getHardwiredChime call to fill in
|
|
19852
|
+
* missing type/time fields, avoiding a double round-trip on every set.
|
|
19853
|
+
* Falls back to fetching if no cache is available.
|
|
19854
|
+
*
|
|
19855
|
+
* @param params - Chime configuration (type, enabled, time)
|
|
19856
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19857
|
+
*/
|
|
19858
|
+
async setHardwiredChime(params, channel) {
|
|
19859
|
+
const ch = this.normalizeChannel(channel);
|
|
19860
|
+
let current = this._hardwiredChimeCache.get(ch);
|
|
19861
|
+
if (!current) {
|
|
19862
|
+
current = await this.getHardwiredChime(ch);
|
|
19863
|
+
}
|
|
19864
|
+
const chimeType = params.type ?? current.type;
|
|
19865
|
+
const enabled = params.enabled ? 1 : 0;
|
|
19866
|
+
const time = params.time ?? current.time;
|
|
19867
|
+
const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
|
|
19868
|
+
const xml = await this.sendXml({
|
|
19869
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
19870
|
+
channel: ch,
|
|
19871
|
+
payloadXml
|
|
19872
|
+
});
|
|
19873
|
+
const newState = parseHardwiredChimeFromXml(xml);
|
|
19874
|
+
this._hardwiredChimeCache.set(ch, newState);
|
|
19875
|
+
return newState;
|
|
19876
|
+
}
|
|
19877
|
+
/**
|
|
19878
|
+
* Play an audio file on the doorbell / chime device.
|
|
19879
|
+
* cmd_id: 349 (QuickReplyPlay)
|
|
19880
|
+
*
|
|
19881
|
+
* @param fileId - The audio file ID to play
|
|
19882
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19883
|
+
*/
|
|
19884
|
+
async quickReplyPlay(fileId, channel) {
|
|
19885
|
+
const ch = this.normalizeChannel(channel);
|
|
19886
|
+
const payloadXml = buildQuickReplyPlayXml(ch, fileId);
|
|
19887
|
+
await this.sendXml({
|
|
19888
|
+
cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
19889
|
+
channel: ch,
|
|
19890
|
+
payloadXml
|
|
19891
|
+
});
|
|
19892
|
+
}
|
|
19893
|
+
/**
|
|
19894
|
+
* Get the silent mode state of a paired wireless chime.
|
|
19895
|
+
* cmd_id: 609 (GetDingDongSilent)
|
|
19896
|
+
*
|
|
19897
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
19898
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19899
|
+
* @returns Wireless chime silent state (time=0 means active/not silenced)
|
|
19900
|
+
*/
|
|
19901
|
+
async getDingDongSilent(chimeId, channel) {
|
|
19902
|
+
const ch = this.normalizeChannel(channel);
|
|
19903
|
+
const payloadXml = buildGetDingDongSilentXml(chimeId);
|
|
19904
|
+
const xml = await this.sendXml({
|
|
19905
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
19906
|
+
channel: ch,
|
|
19907
|
+
payloadXml
|
|
19908
|
+
});
|
|
19909
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
19910
|
+
}
|
|
19911
|
+
/**
|
|
19912
|
+
* Set the silent mode of a paired wireless chime.
|
|
19913
|
+
* cmd_id: 610 (SetDingDongSilent)
|
|
19914
|
+
*
|
|
19915
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
19916
|
+
* @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
|
|
19917
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19918
|
+
* @returns Updated wireless chime silent state
|
|
19919
|
+
*/
|
|
19920
|
+
async setDingDongSilent(chimeId, time, channel) {
|
|
19921
|
+
const ch = this.normalizeChannel(channel);
|
|
19922
|
+
const payloadXml = buildSetDingDongSilentXml(chimeId, time);
|
|
19923
|
+
const xml = await this.sendXml({
|
|
19924
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
19925
|
+
channel: ch,
|
|
19926
|
+
payloadXml
|
|
19927
|
+
});
|
|
19928
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
19929
|
+
}
|
|
19200
19930
|
};
|
|
19201
19931
|
|
|
19202
19932
|
// src/reolink/discovery.ts
|
|
@@ -20194,6 +20924,7 @@ export {
|
|
|
20194
20924
|
flattenAbilitiesForChannel,
|
|
20195
20925
|
abilitiesHasAny,
|
|
20196
20926
|
parseSupportXml,
|
|
20927
|
+
getSupportItemForChannel,
|
|
20197
20928
|
computeDeviceCapabilities,
|
|
20198
20929
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
20199
20930
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
@@ -20212,4 +20943,4 @@ export {
|
|
|
20212
20943
|
isTcpFailureThatShouldFallbackToUdp,
|
|
20213
20944
|
autoDetectDeviceType
|
|
20214
20945
|
};
|
|
20215
|
-
//# sourceMappingURL=chunk-
|
|
20946
|
+
//# sourceMappingURL=chunk-MN7GUZT7.js.map
|