@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
|
@@ -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) {
|
|
@@ -5392,6 +5400,8 @@ var NativeStreamFanout = class {
|
|
|
5392
5400
|
} finally {
|
|
5393
5401
|
for (const q of this.queues.values()) q.close();
|
|
5394
5402
|
this.queues.clear();
|
|
5403
|
+
this.running = false;
|
|
5404
|
+
this.opts.onEnd?.();
|
|
5395
5405
|
}
|
|
5396
5406
|
})();
|
|
5397
5407
|
}
|
|
@@ -5491,6 +5501,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5491
5501
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
5492
5502
|
nativeFanout = null;
|
|
5493
5503
|
noClientAutoStopTimer;
|
|
5504
|
+
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
5505
|
+
// When a new client connects while the stream is already running it does not need
|
|
5506
|
+
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
5507
|
+
// from the last IDR in the prebuffer immediately.
|
|
5508
|
+
PREBUFFER_MAX_MS = 3e3;
|
|
5509
|
+
prebuffer = [];
|
|
5494
5510
|
static isAdtsAacFrame(b) {
|
|
5495
5511
|
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
5496
5512
|
}
|
|
@@ -5525,6 +5541,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5525
5541
|
);
|
|
5526
5542
|
return { sampleRate, channels, configHex };
|
|
5527
5543
|
}
|
|
5544
|
+
/** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
|
|
5545
|
+
isRawFrameKeyframe(frame) {
|
|
5546
|
+
try {
|
|
5547
|
+
if (frame.videoType === "H264") {
|
|
5548
|
+
const nals = _BaichuanRtspServer.splitAnnexBNals(
|
|
5549
|
+
convertToAnnexB(frame.data)
|
|
5550
|
+
);
|
|
5551
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
5552
|
+
}
|
|
5553
|
+
if (frame.videoType === "H265") {
|
|
5554
|
+
const nals = splitAnnexBToNalPayloads2(convertToAnnexB2(frame.data));
|
|
5555
|
+
return nals.some(
|
|
5556
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
5557
|
+
);
|
|
5558
|
+
}
|
|
5559
|
+
} catch {
|
|
5560
|
+
}
|
|
5561
|
+
return false;
|
|
5562
|
+
}
|
|
5528
5563
|
static parseInterleavedChannels(transportHeader) {
|
|
5529
5564
|
const m = transportHeader.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
|
|
5530
5565
|
if (!m) return null;
|
|
@@ -5717,7 +5752,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5717
5752
|
this.logger.warn(
|
|
5718
5753
|
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
5719
5754
|
);
|
|
5720
|
-
this.streamMetadata = { frameRate: 25
|
|
5755
|
+
this.streamMetadata = { frameRate: 25 };
|
|
5721
5756
|
this.setFlowVideoType("H264", "metadata unavailable");
|
|
5722
5757
|
}
|
|
5723
5758
|
this.clientConnectionServer = net2.createServer((socket) => {
|
|
@@ -5749,7 +5784,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5749
5784
|
*/
|
|
5750
5785
|
handleRtspConnection(socket) {
|
|
5751
5786
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
5752
|
-
|
|
5787
|
+
const connectTime = Date.now();
|
|
5788
|
+
this.logger.info(
|
|
5789
|
+
`[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
|
|
5790
|
+
);
|
|
5753
5791
|
let sessionId = "";
|
|
5754
5792
|
let buffer = Buffer.alloc(0);
|
|
5755
5793
|
let clientFfmpeg;
|
|
@@ -5757,6 +5795,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5757
5795
|
let clientUdpSocket = null;
|
|
5758
5796
|
let clientUdpSocketAudio = null;
|
|
5759
5797
|
const cleanup = () => {
|
|
5798
|
+
const sessionDurationMs = Date.now() - connectTime;
|
|
5799
|
+
const res = this.clientResources.get(clientId);
|
|
5800
|
+
const framesSent = res?.framesSent ?? 0;
|
|
5801
|
+
this.logger.info(
|
|
5802
|
+
`[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
|
|
5803
|
+
);
|
|
5760
5804
|
this.removeClient(clientId);
|
|
5761
5805
|
this.authNonces.delete(clientId);
|
|
5762
5806
|
const resources = this.clientResources.get(clientId);
|
|
@@ -5898,7 +5942,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5898
5942
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
5899
5943
|
});
|
|
5900
5944
|
} else if (method === "DESCRIBE") {
|
|
5901
|
-
if (!this.
|
|
5945
|
+
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
5902
5946
|
try {
|
|
5903
5947
|
if (!this.nativeStreamActive) {
|
|
5904
5948
|
await this.startNativeStream();
|
|
@@ -5910,7 +5954,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5910
5954
|
}
|
|
5911
5955
|
const { hasParamSets } = this.flow.getFmtp();
|
|
5912
5956
|
if (!hasParamSets) {
|
|
5913
|
-
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 :
|
|
5957
|
+
const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 3e3;
|
|
5958
|
+
const primingStart = Date.now();
|
|
5959
|
+
this.logger.info(
|
|
5960
|
+
`[rebroadcast] DESCRIBE priming: waiting up to ${primingMs}ms for SPS/PPS client=${clientId} path=${this.path}`
|
|
5961
|
+
);
|
|
5914
5962
|
try {
|
|
5915
5963
|
await Promise.race([
|
|
5916
5964
|
this.firstFramePromise || Promise.resolve(),
|
|
@@ -5918,6 +5966,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5918
5966
|
]);
|
|
5919
5967
|
} catch {
|
|
5920
5968
|
}
|
|
5969
|
+
const primingElapsed = Date.now() - primingStart;
|
|
5970
|
+
const { hasParamSets: hasParamSetsAfter } = this.flow.getFmtp();
|
|
5971
|
+
if (hasParamSetsAfter) {
|
|
5972
|
+
this.logger.info(
|
|
5973
|
+
`[rebroadcast] DESCRIBE priming: SPS/PPS received after ${primingElapsed}ms client=${clientId} path=${this.path}`
|
|
5974
|
+
);
|
|
5975
|
+
} else {
|
|
5976
|
+
this.logger.warn(
|
|
5977
|
+
`[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}`
|
|
5978
|
+
);
|
|
5979
|
+
}
|
|
5921
5980
|
}
|
|
5922
5981
|
}
|
|
5923
5982
|
{
|
|
@@ -5926,11 +5985,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5926
5985
|
this.logger.info(
|
|
5927
5986
|
`[BaichuanRtspServer] DESCRIBE SDP for ${clientId} path=${this.path} codec=${this.flow.sdpCodec} hasParamSets=${hasParamSets} fmtp=${fmtpPreview}`
|
|
5928
5987
|
);
|
|
5929
|
-
if (!hasParamSets) {
|
|
5930
|
-
this.rtspDebugLog(
|
|
5931
|
-
`DESCRIBE responding without parameter sets yet (client=${clientId}, path=${this.path}, flow=${this.flow.key})`
|
|
5932
|
-
);
|
|
5933
|
-
}
|
|
5934
5988
|
}
|
|
5935
5989
|
const sdp = this.generateSdp();
|
|
5936
5990
|
sendResponse(
|
|
@@ -5980,7 +6034,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5980
6034
|
seenFirstVideoKeyframe: false,
|
|
5981
6035
|
setupTrack0: false,
|
|
5982
6036
|
setupTrack1: false,
|
|
5983
|
-
isPlaying: false
|
|
6037
|
+
isPlaying: false,
|
|
6038
|
+
connectTime
|
|
5984
6039
|
});
|
|
5985
6040
|
} else {
|
|
5986
6041
|
existing.rtspSocket = socket;
|
|
@@ -6027,8 +6082,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6027
6082
|
if (resources) {
|
|
6028
6083
|
if (isTrack1) resources.setupTrack1 = true;
|
|
6029
6084
|
else resources.setupTrack0 = true;
|
|
6030
|
-
|
|
6031
|
-
|
|
6085
|
+
const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
|
|
6086
|
+
const track = isTrack1 ? "track1(audio)" : "track0(video)";
|
|
6087
|
+
this.logger.info(
|
|
6088
|
+
`[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
|
|
6032
6089
|
);
|
|
6033
6090
|
}
|
|
6034
6091
|
}
|
|
@@ -6053,8 +6110,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6053
6110
|
const resources = this.clientResources.get(clientId);
|
|
6054
6111
|
if (resources) {
|
|
6055
6112
|
resources.isPlaying = true;
|
|
6056
|
-
|
|
6057
|
-
|
|
6113
|
+
const hasAudio = !!resources.setupTrack1;
|
|
6114
|
+
this.logger.info(
|
|
6115
|
+
`[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
|
|
6058
6116
|
);
|
|
6059
6117
|
}
|
|
6060
6118
|
}
|
|
@@ -6063,6 +6121,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6063
6121
|
Range: "npt=0.000-"
|
|
6064
6122
|
});
|
|
6065
6123
|
} else if (method === "TEARDOWN") {
|
|
6124
|
+
this.logger.info(
|
|
6125
|
+
`[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
|
|
6126
|
+
);
|
|
6066
6127
|
cleanup();
|
|
6067
6128
|
sendResponse(200, "OK", {
|
|
6068
6129
|
Session: sessionId
|
|
@@ -6128,10 +6189,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6128
6189
|
sdp += `a=control:track1\r
|
|
6129
6190
|
`;
|
|
6130
6191
|
}
|
|
6131
|
-
sdp += `a=setup:passive\r
|
|
6132
|
-
`;
|
|
6133
|
-
sdp += `a=connection:new\r
|
|
6134
|
-
`;
|
|
6135
6192
|
return sdp;
|
|
6136
6193
|
}
|
|
6137
6194
|
/**
|
|
@@ -6157,7 +6214,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6157
6214
|
this.logger.warn(
|
|
6158
6215
|
`[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
|
|
6159
6216
|
);
|
|
6160
|
-
streamMetadata = { frameRate: 25
|
|
6217
|
+
streamMetadata = { frameRate: 25 };
|
|
6161
6218
|
}
|
|
6162
6219
|
}
|
|
6163
6220
|
const ffmpegFormat = this.flow.ffmpegFormat;
|
|
@@ -6202,6 +6259,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6202
6259
|
return false;
|
|
6203
6260
|
if (channel === audioRtpChannel && !resources2?.setupTrack1)
|
|
6204
6261
|
return false;
|
|
6262
|
+
const buffered = rtspSocket.writableLength;
|
|
6263
|
+
if (buffered > 10 * 1024 * 1024) {
|
|
6264
|
+
this.logger.warn(
|
|
6265
|
+
`[rebroadcast] backpressure: ${Math.round(buffered / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
|
|
6266
|
+
);
|
|
6267
|
+
rtspSocket.destroy();
|
|
6268
|
+
return false;
|
|
6269
|
+
}
|
|
6205
6270
|
try {
|
|
6206
6271
|
return rtspSocket.write(frameRtpOverTcp(channel, msg));
|
|
6207
6272
|
} catch (error) {
|
|
@@ -6631,6 +6696,24 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6631
6696
|
let frameCount = 0;
|
|
6632
6697
|
let lastFrameTime = Date.now();
|
|
6633
6698
|
const targetFrameInterval = streamMetadata && streamMetadata.frameRate > 0 ? 1e3 / streamMetadata.frameRate : 40;
|
|
6699
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
6700
|
+
let lastIdrIdx = -1;
|
|
6701
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
6702
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
6703
|
+
lastIdrIdx = i;
|
|
6704
|
+
break;
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
const prebufferFrames = lastIdrIdx >= 0 ? prebufferSnap.slice(lastIdrIdx) : [];
|
|
6708
|
+
if (prebufferFrames.length > 0) {
|
|
6709
|
+
this.logger.info(
|
|
6710
|
+
`[rebroadcast] prebuffer replay client=${clientId} frames=${prebufferFrames.length} starting from IDR`
|
|
6711
|
+
);
|
|
6712
|
+
}
|
|
6713
|
+
const combined = async function* () {
|
|
6714
|
+
for (const entry of prebufferFrames) yield entry.frame;
|
|
6715
|
+
for await (const f of clientGenerator) yield f;
|
|
6716
|
+
};
|
|
6634
6717
|
const feedFrames = async () => {
|
|
6635
6718
|
try {
|
|
6636
6719
|
this.rtspDebugLog(
|
|
@@ -6642,7 +6725,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6642
6725
|
let firstVideoFrameSeenLogged = false;
|
|
6643
6726
|
let h265WaitParamSetsLogged = false;
|
|
6644
6727
|
let h265WaitIrapLogged = false;
|
|
6645
|
-
for await (const frame of
|
|
6728
|
+
for await (const frame of combined()) {
|
|
6646
6729
|
if (!this.connectedClients.has(clientId)) {
|
|
6647
6730
|
this.rtspDebugLog(
|
|
6648
6731
|
`Client ${clientId} disconnected, stopping frame feed`
|
|
@@ -6745,15 +6828,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6745
6828
|
`Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
|
|
6746
6829
|
);
|
|
6747
6830
|
}
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6831
|
+
if (!useDirectRtp) {
|
|
6832
|
+
const now = Date.now();
|
|
6833
|
+
const timeSinceLastFrame = now - lastFrameTime;
|
|
6834
|
+
const waitTime = targetFrameInterval - timeSinceLastFrame;
|
|
6835
|
+
if (waitTime > 0) {
|
|
6836
|
+
await new Promise(
|
|
6837
|
+
(resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
|
|
6838
|
+
);
|
|
6839
|
+
}
|
|
6840
|
+
lastFrameTime = Date.now();
|
|
6755
6841
|
}
|
|
6756
|
-
lastFrameTime = Date.now();
|
|
6757
6842
|
if (useDirectRtp) {
|
|
6758
6843
|
const videoType = frame.videoType ?? this.flow.videoType;
|
|
6759
6844
|
const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
|
|
@@ -6826,6 +6911,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6826
6911
|
}
|
|
6827
6912
|
if (!firstVideoWriteLogged) {
|
|
6828
6913
|
firstVideoWriteLogged = true;
|
|
6914
|
+
const clientConnectTime = resources?.connectTime ?? Date.now();
|
|
6915
|
+
const ttffMs = Date.now() - clientConnectTime;
|
|
6916
|
+
this.logger.info(
|
|
6917
|
+
`[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
|
|
6918
|
+
);
|
|
6829
6919
|
if (rtspDebug) {
|
|
6830
6920
|
const headHex = frame.data.subarray(0, 16).toString("hex");
|
|
6831
6921
|
rtspDebugLog(
|
|
@@ -6833,6 +6923,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6833
6923
|
);
|
|
6834
6924
|
}
|
|
6835
6925
|
}
|
|
6926
|
+
if (resources) {
|
|
6927
|
+
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
6928
|
+
}
|
|
6836
6929
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
6837
6930
|
} else {
|
|
6838
6931
|
try {
|
|
@@ -6917,8 +7010,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6917
7010
|
this.firstAudioPromise = new Promise((resolve) => {
|
|
6918
7011
|
this.firstAudioResolve = resolve;
|
|
6919
7012
|
});
|
|
6920
|
-
this.
|
|
6921
|
-
`
|
|
7013
|
+
this.logger.info(
|
|
7014
|
+
`[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
6922
7015
|
);
|
|
6923
7016
|
await this.flow.startKeepAlive(this.api);
|
|
6924
7017
|
this.nativeFanout = new NativeStreamFanout({
|
|
@@ -6956,11 +7049,41 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6956
7049
|
if (hasParamSets) {
|
|
6957
7050
|
this.markFirstFrameReceived();
|
|
6958
7051
|
}
|
|
7052
|
+
const isKeyframe = this.isRawFrameKeyframe(frame);
|
|
7053
|
+
this.prebuffer.push({
|
|
7054
|
+
frame: { ...frame, data: Buffer.from(frame.data) },
|
|
7055
|
+
time: Date.now(),
|
|
7056
|
+
isKeyframe
|
|
7057
|
+
});
|
|
7058
|
+
const cutoff = Date.now() - this.PREBUFFER_MAX_MS;
|
|
7059
|
+
let trimIdx = 0;
|
|
7060
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
7061
|
+
trimIdx++;
|
|
7062
|
+
}
|
|
7063
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
6959
7064
|
},
|
|
6960
7065
|
onError: (error) => {
|
|
6961
7066
|
this.logger.warn(
|
|
6962
7067
|
`[BaichuanRtspServer] Shared native stream error: ${error}`
|
|
6963
7068
|
);
|
|
7069
|
+
},
|
|
7070
|
+
onEnd: () => {
|
|
7071
|
+
if (!this.nativeStreamActive) return;
|
|
7072
|
+
this.nativeStreamActive = false;
|
|
7073
|
+
this.firstFrameReceived = false;
|
|
7074
|
+
this.firstFramePromise = null;
|
|
7075
|
+
this.firstFrameResolve = null;
|
|
7076
|
+
this.nativeFanout = null;
|
|
7077
|
+
this.prebuffer = [];
|
|
7078
|
+
this.logger.info(
|
|
7079
|
+
`[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
7080
|
+
);
|
|
7081
|
+
if (this.connectedClients.size > 0) {
|
|
7082
|
+
this.logger.info(
|
|
7083
|
+
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
7084
|
+
);
|
|
7085
|
+
setImmediate(() => void this.startNativeStream());
|
|
7086
|
+
}
|
|
6964
7087
|
}
|
|
6965
7088
|
});
|
|
6966
7089
|
this.nativeFanout.start();
|
|
@@ -6999,7 +7122,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6999
7122
|
if (!this.nativeStreamActive) {
|
|
7000
7123
|
return;
|
|
7001
7124
|
}
|
|
7002
|
-
this.
|
|
7125
|
+
this.logger.info(
|
|
7126
|
+
`[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
|
|
7127
|
+
);
|
|
7003
7128
|
this.flow.stopKeepAlive();
|
|
7004
7129
|
this.clearNoClientAutoStopTimer();
|
|
7005
7130
|
this.nativeStreamActive = false;
|
|
@@ -7018,6 +7143,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7018
7143
|
this.nativeFanout = null;
|
|
7019
7144
|
await fanout.stop();
|
|
7020
7145
|
}
|
|
7146
|
+
this.prebuffer = [];
|
|
7021
7147
|
if (this.tempStreamGenerator) {
|
|
7022
7148
|
try {
|
|
7023
7149
|
await this.tempStreamGenerator.return(void 0);
|
|
@@ -7033,9 +7159,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7033
7159
|
if (this.connectedClients.has(clientId)) {
|
|
7034
7160
|
this.connectedClients.delete(clientId);
|
|
7035
7161
|
this.emit("clientDisconnected", clientId);
|
|
7036
|
-
this.logger.info(
|
|
7037
|
-
`[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
|
|
7038
|
-
);
|
|
7039
7162
|
if (this.connectedClients.size === 0) {
|
|
7040
7163
|
void this.stopNativeStream();
|
|
7041
7164
|
}
|
|
@@ -7268,10 +7391,12 @@ function parseSupportXml(xml) {
|
|
|
7268
7391
|
}
|
|
7269
7392
|
function getSupportItemForChannel(support, channel) {
|
|
7270
7393
|
if (!support?.items?.length) return void 0;
|
|
7271
|
-
const
|
|
7394
|
+
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
7395
|
+
if (!candidates.length) return void 0;
|
|
7396
|
+
const score = (item) => {
|
|
7272
7397
|
const anyItem = item;
|
|
7273
|
-
let
|
|
7274
|
-
if (anyItem.name == null)
|
|
7398
|
+
let result = 0;
|
|
7399
|
+
if (anyItem.name == null) result += 100;
|
|
7275
7400
|
const capabilityKeys = [
|
|
7276
7401
|
"ptzType",
|
|
7277
7402
|
"ptzControl",
|
|
@@ -7283,20 +7408,17 @@ function getSupportItemForChannel(support, channel) {
|
|
|
7283
7408
|
"motion",
|
|
7284
7409
|
"encCtrl",
|
|
7285
7410
|
"newIspCfg",
|
|
7286
|
-
"remoteAbility"
|
|
7411
|
+
"remoteAbility",
|
|
7412
|
+
"aitype",
|
|
7413
|
+
"videoClip",
|
|
7414
|
+
"snap"
|
|
7287
7415
|
];
|
|
7288
7416
|
for (const k of capabilityKeys) {
|
|
7289
|
-
if (anyItem[k] !== void 0)
|
|
7417
|
+
if (anyItem[k] !== void 0) result += 3;
|
|
7290
7418
|
}
|
|
7291
|
-
|
|
7292
|
-
return score;
|
|
7293
|
-
};
|
|
7294
|
-
const pickBest = (chnId) => {
|
|
7295
|
-
const candidates = support.items.filter((i) => i.chnID === chnId);
|
|
7296
|
-
if (!candidates.length) return void 0;
|
|
7297
|
-
return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
|
|
7419
|
+
return result;
|
|
7298
7420
|
};
|
|
7299
|
-
return
|
|
7421
|
+
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
7300
7422
|
}
|
|
7301
7423
|
function computeDeviceCapabilities(params) {
|
|
7302
7424
|
const { channel } = params;
|
|
@@ -7328,6 +7450,7 @@ function computeDeviceCapabilities(params) {
|
|
|
7328
7450
|
flat,
|
|
7329
7451
|
/white\s*led|whiteLed|flood\s*light|floodlight/i
|
|
7330
7452
|
);
|
|
7453
|
+
const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
|
|
7331
7454
|
const hasSirenFromAbilities = abilitiesHasAny(
|
|
7332
7455
|
flat,
|
|
7333
7456
|
/audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
|
|
@@ -7340,6 +7463,9 @@ function computeDeviceCapabilities(params) {
|
|
|
7340
7463
|
const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
|
|
7341
7464
|
const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
|
|
7342
7465
|
const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
|
|
7466
|
+
const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
|
|
7467
|
+
const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
|
|
7468
|
+
const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
|
|
7343
7469
|
const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
7344
7470
|
const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
|
|
7345
7471
|
const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
|
|
@@ -7355,14 +7481,15 @@ function computeDeviceCapabilities(params) {
|
|
|
7355
7481
|
hasZoom: finalHasZoom,
|
|
7356
7482
|
hasPresets: finalHasPresets,
|
|
7357
7483
|
hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
|
|
7358
|
-
hasBattery
|
|
7484
|
+
hasBattery,
|
|
7359
7485
|
hasIntercom: hasIntercomFromSupport,
|
|
7360
|
-
hasSiren: hasSirenFromAbilities,
|
|
7486
|
+
hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
|
|
7361
7487
|
// lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
|
|
7362
7488
|
hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
|
|
7363
7489
|
hasPir: hasPirFromAbilities || hasPirFromSupport,
|
|
7364
|
-
isDoorbell
|
|
7365
|
-
hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
|
|
7490
|
+
isDoorbell,
|
|
7491
|
+
hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
|
|
7492
|
+
hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
|
|
7366
7493
|
};
|
|
7367
7494
|
if (ptzMode !== void 0) result.ptzMode = ptzMode;
|
|
7368
7495
|
return result;
|
|
@@ -9237,6 +9364,161 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
|
|
|
9237
9364
|
return extractReolinkUidLike(p2pXml);
|
|
9238
9365
|
};
|
|
9239
9366
|
|
|
9367
|
+
// src/reolink/baichuan/utils/chime.ts
|
|
9368
|
+
var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9369
|
+
<body>
|
|
9370
|
+
<dingdongDeviceOpt version="1.1">
|
|
9371
|
+
<id>${chimeId}</id>
|
|
9372
|
+
<opt>getParam</opt>
|
|
9373
|
+
</dingdongDeviceOpt>
|
|
9374
|
+
</body>`;
|
|
9375
|
+
var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9376
|
+
<body>
|
|
9377
|
+
<dingdongDeviceOpt version="1.1">
|
|
9378
|
+
<opt>setParam</opt>
|
|
9379
|
+
<id>${chimeId}</id>
|
|
9380
|
+
${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
|
|
9381
|
+
${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
|
|
9382
|
+
${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
|
|
9383
|
+
</dingdongDeviceOpt>
|
|
9384
|
+
</body>`;
|
|
9385
|
+
var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9386
|
+
<body>
|
|
9387
|
+
<dingdongDeviceOpt version="1.1">
|
|
9388
|
+
<id>${chimeId}</id>
|
|
9389
|
+
<opt>ringWithMusic</opt>
|
|
9390
|
+
<musicId>${musicId}</musicId>
|
|
9391
|
+
</dingdongDeviceOpt>
|
|
9392
|
+
</body>`;
|
|
9393
|
+
var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9394
|
+
<body>
|
|
9395
|
+
<dingdongCfg version="1.1">
|
|
9396
|
+
<deviceCfg>
|
|
9397
|
+
<id>${chimeId}</id>
|
|
9398
|
+
<alarminCfg>
|
|
9399
|
+
<valid>${state}</valid>
|
|
9400
|
+
<musicId>${musicId}</musicId>
|
|
9401
|
+
<type>${eventType}</type>
|
|
9402
|
+
</alarminCfg>
|
|
9403
|
+
</deviceCfg>
|
|
9404
|
+
</dingdongCfg>
|
|
9405
|
+
</body>`;
|
|
9406
|
+
var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9407
|
+
<body>
|
|
9408
|
+
<dingdongCtrl version="1.1">
|
|
9409
|
+
<opt>machineStateGet</opt>
|
|
9410
|
+
</dingdongCtrl>
|
|
9411
|
+
</body>`;
|
|
9412
|
+
var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9413
|
+
<body>
|
|
9414
|
+
<dingdongCtrl version="1.1">
|
|
9415
|
+
<opt>machineStateSet</opt>
|
|
9416
|
+
<type>${chimeType}</type>
|
|
9417
|
+
<bopen>${enabled}</bopen>
|
|
9418
|
+
<bsave>1</bsave>
|
|
9419
|
+
<time>${time}</time>
|
|
9420
|
+
</dingdongCtrl>
|
|
9421
|
+
</body>`;
|
|
9422
|
+
var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9423
|
+
<body>
|
|
9424
|
+
<audioFileInfo version="1.1">
|
|
9425
|
+
<channelId>${channel}</channelId>
|
|
9426
|
+
<id>${fileId}</id>
|
|
9427
|
+
<timeout>0</timeout>
|
|
9428
|
+
</audioFileInfo>
|
|
9429
|
+
</body>`;
|
|
9430
|
+
var parseDingDongListFromXml = (xml) => {
|
|
9431
|
+
const devices = [];
|
|
9432
|
+
const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
|
|
9433
|
+
for (const block of blocks) {
|
|
9434
|
+
const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
|
|
9435
|
+
const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
|
|
9436
|
+
const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
|
|
9437
|
+
if (idText === void 0) continue;
|
|
9438
|
+
const id = Number(idText);
|
|
9439
|
+
if (!Number.isFinite(id)) continue;
|
|
9440
|
+
devices.push({
|
|
9441
|
+
id,
|
|
9442
|
+
name,
|
|
9443
|
+
netState: netStateText !== void 0 ? Number(netStateText) : 0
|
|
9444
|
+
});
|
|
9445
|
+
}
|
|
9446
|
+
return devices;
|
|
9447
|
+
};
|
|
9448
|
+
var parseDingDongParamsFromXml = (xml) => {
|
|
9449
|
+
const name = getXmlText(xml, "name");
|
|
9450
|
+
const volLevelText = getXmlText(xml, "volLevel");
|
|
9451
|
+
const ledStateText = getXmlText(xml, "ledState");
|
|
9452
|
+
const result = {};
|
|
9453
|
+
if (name !== void 0) result.name = name;
|
|
9454
|
+
if (volLevelText !== void 0) {
|
|
9455
|
+
const n = Number(volLevelText);
|
|
9456
|
+
if (Number.isFinite(n)) result.volLevel = n;
|
|
9457
|
+
}
|
|
9458
|
+
if (ledStateText !== void 0) {
|
|
9459
|
+
const n = Number(ledStateText);
|
|
9460
|
+
if (Number.isFinite(n)) result.ledState = n;
|
|
9461
|
+
}
|
|
9462
|
+
return result;
|
|
9463
|
+
};
|
|
9464
|
+
var parseDingDongCfgFromXml = (xml) => {
|
|
9465
|
+
const configs = [];
|
|
9466
|
+
const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
|
|
9467
|
+
for (const deviceBlock of deviceBlocks) {
|
|
9468
|
+
const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
|
|
9469
|
+
if (idText === void 0) continue;
|
|
9470
|
+
const id = Number(idText);
|
|
9471
|
+
if (!Number.isFinite(id)) continue;
|
|
9472
|
+
const typeMap = {};
|
|
9473
|
+
const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
|
|
9474
|
+
for (const alarmBlock of alarmBlocks) {
|
|
9475
|
+
const type = getXmlText(alarmBlock, "type");
|
|
9476
|
+
if (!type) continue;
|
|
9477
|
+
const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
|
|
9478
|
+
const musicIdText = getXmlText(alarmBlock, "musicId");
|
|
9479
|
+
typeMap[type] = {
|
|
9480
|
+
valid: validText !== void 0 ? Number(validText) : 0,
|
|
9481
|
+
musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
|
|
9482
|
+
};
|
|
9483
|
+
}
|
|
9484
|
+
configs.push({ id, type: typeMap });
|
|
9485
|
+
}
|
|
9486
|
+
return configs;
|
|
9487
|
+
};
|
|
9488
|
+
var parseHardwiredChimeFromXml = (xml) => {
|
|
9489
|
+
const type = getXmlText(xml, "type") ?? "";
|
|
9490
|
+
const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
|
|
9491
|
+
const timeText = getXmlText(xml, "time");
|
|
9492
|
+
return {
|
|
9493
|
+
type,
|
|
9494
|
+
enabled: bopenText === "1",
|
|
9495
|
+
time: timeText !== void 0 ? Number(timeText) : 0
|
|
9496
|
+
};
|
|
9497
|
+
};
|
|
9498
|
+
var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9499
|
+
<body>
|
|
9500
|
+
<dingdongSilentMode version="1.1">
|
|
9501
|
+
<id>${chimeId}</id>
|
|
9502
|
+
</dingdongSilentMode>
|
|
9503
|
+
</body>`;
|
|
9504
|
+
var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
|
|
9505
|
+
<body>
|
|
9506
|
+
<dingdongSilentMode version="1.1">
|
|
9507
|
+
<id>${chimeId}</id>
|
|
9508
|
+
<time>${time}</time>
|
|
9509
|
+
<type>63</type>
|
|
9510
|
+
</dingdongSilentMode>
|
|
9511
|
+
</body>`;
|
|
9512
|
+
var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
|
|
9513
|
+
const timeText = getXmlText(xml, "time");
|
|
9514
|
+
const time = timeText !== void 0 ? Number(timeText) : 0;
|
|
9515
|
+
return {
|
|
9516
|
+
id: chimeId,
|
|
9517
|
+
time,
|
|
9518
|
+
active: time === 0
|
|
9519
|
+
};
|
|
9520
|
+
};
|
|
9521
|
+
|
|
9240
9522
|
// src/reolink/baichuan/utils/eventsGetEvents.ts
|
|
9241
9523
|
var parseAiTypeToken = (aiTypeRaw) => {
|
|
9242
9524
|
const raw = (aiTypeRaw ?? "").trim();
|
|
@@ -9546,6 +9828,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9546
9828
|
host;
|
|
9547
9829
|
username;
|
|
9548
9830
|
password;
|
|
9831
|
+
/**
|
|
9832
|
+
* Set to `true` after `close()` is called.
|
|
9833
|
+
* Once closed, the API instance should not be reused.
|
|
9834
|
+
*/
|
|
9835
|
+
_closed = false;
|
|
9549
9836
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9550
9837
|
// SOCKET POOL - Tag-based socket management
|
|
9551
9838
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -9575,10 +9862,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
9575
9862
|
get client() {
|
|
9576
9863
|
const entry = this.socketPool.get("general");
|
|
9577
9864
|
if (!entry) {
|
|
9865
|
+
if (this._closed) {
|
|
9866
|
+
throw new Error(
|
|
9867
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
9868
|
+
);
|
|
9869
|
+
}
|
|
9578
9870
|
throw new Error("[ReolinkBaichuanApi] General socket not initialized");
|
|
9579
9871
|
}
|
|
9580
9872
|
return entry.client;
|
|
9581
9873
|
}
|
|
9874
|
+
/**
|
|
9875
|
+
* `true` after `close()` has been called. A closed API should not be reused;
|
|
9876
|
+
* the consumer should create a new instance.
|
|
9877
|
+
*/
|
|
9878
|
+
get isClosed() {
|
|
9879
|
+
return this._closed;
|
|
9880
|
+
}
|
|
9881
|
+
/**
|
|
9882
|
+
* `true` when the API is usable: not closed, general socket exists, socket
|
|
9883
|
+
* is connected and the client is logged in.
|
|
9884
|
+
*
|
|
9885
|
+
* This is the recommended way for consumers to check whether the API is
|
|
9886
|
+
* still valid before issuing commands, instead of directly accessing
|
|
9887
|
+
* `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
|
|
9888
|
+
* if the socket pool was already destroyed).
|
|
9889
|
+
*/
|
|
9890
|
+
get isReady() {
|
|
9891
|
+
if (this._closed) return false;
|
|
9892
|
+
const entry = this.socketPool.get("general");
|
|
9893
|
+
if (!entry) return false;
|
|
9894
|
+
try {
|
|
9895
|
+
return entry.client.isSocketConnected() && entry.client.loggedIn;
|
|
9896
|
+
} catch {
|
|
9897
|
+
return false;
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
/** Promise tracking an in-flight reconnection from `ensureConnected()`. */
|
|
9901
|
+
_ensureConnectedPromise;
|
|
9902
|
+
/**
|
|
9903
|
+
* Ensure the "general" socket is connected and logged in.
|
|
9904
|
+
* If the socket is disconnected or the pool entry was destroyed, a new
|
|
9905
|
+
* general socket is created, logged in, and all event/push/guard listeners
|
|
9906
|
+
* are re-attached automatically.
|
|
9907
|
+
*
|
|
9908
|
+
* This is a **no-op** when the API is already {@link isReady}.
|
|
9909
|
+
*
|
|
9910
|
+
* @throws If `close()` was called — the API is permanently closed and a new
|
|
9911
|
+
* instance must be created.
|
|
9912
|
+
*/
|
|
9913
|
+
async ensureConnected() {
|
|
9914
|
+
if (this._closed) {
|
|
9915
|
+
throw new Error(
|
|
9916
|
+
"[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
|
|
9917
|
+
);
|
|
9918
|
+
}
|
|
9919
|
+
if (this.isReady) return;
|
|
9920
|
+
if (this._ensureConnectedPromise) {
|
|
9921
|
+
return this._ensureConnectedPromise;
|
|
9922
|
+
}
|
|
9923
|
+
this._ensureConnectedPromise = this.reconnectGeneralSocket();
|
|
9924
|
+
try {
|
|
9925
|
+
await this._ensureConnectedPromise;
|
|
9926
|
+
} finally {
|
|
9927
|
+
this._ensureConnectedPromise = void 0;
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
/**
|
|
9931
|
+
* Internal: destroy the current general socket (if any), create a new one,
|
|
9932
|
+
* login, and re-attach all listeners.
|
|
9933
|
+
*/
|
|
9934
|
+
async reconnectGeneralSocket() {
|
|
9935
|
+
const oldEntry = this.socketPool.get("general");
|
|
9936
|
+
if (oldEntry) {
|
|
9937
|
+
oldEntry.client.removeAllListeners();
|
|
9938
|
+
if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
|
|
9939
|
+
if (oldEntry.generalPermitRelease) {
|
|
9940
|
+
try {
|
|
9941
|
+
oldEntry.generalPermitRelease();
|
|
9942
|
+
} catch {
|
|
9943
|
+
}
|
|
9944
|
+
}
|
|
9945
|
+
this.socketPool.delete("general");
|
|
9946
|
+
try {
|
|
9947
|
+
await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
|
|
9948
|
+
} catch {
|
|
9949
|
+
}
|
|
9950
|
+
}
|
|
9951
|
+
const newClient = new BaichuanClient(this.clientOptions);
|
|
9952
|
+
this.socketPool.set("general", {
|
|
9953
|
+
client: newClient,
|
|
9954
|
+
refCount: 1,
|
|
9955
|
+
// general socket is always "in use"
|
|
9956
|
+
createdAt: Date.now(),
|
|
9957
|
+
lastUsedAt: Date.now(),
|
|
9958
|
+
idleCloseTimer: void 0,
|
|
9959
|
+
generalPermitRelease: void 0
|
|
9960
|
+
});
|
|
9961
|
+
this.setupGeneralClientListeners();
|
|
9962
|
+
await this.client.login();
|
|
9963
|
+
this.logger.log?.(
|
|
9964
|
+
"[ReolinkBaichuanApi] General socket reconnected successfully"
|
|
9965
|
+
);
|
|
9966
|
+
if (this.simpleEventListeners.size > 0) {
|
|
9967
|
+
this.simpleEventSubscribed = false;
|
|
9968
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
9969
|
+
this.simpleEventWatchdogLastRecoveryAt = 0;
|
|
9970
|
+
try {
|
|
9971
|
+
await this.ensureSimpleEventSubscribed();
|
|
9972
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
9973
|
+
this.logger.log?.(
|
|
9974
|
+
`[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
|
|
9975
|
+
);
|
|
9976
|
+
} catch (e) {
|
|
9977
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
9978
|
+
this.logger,
|
|
9979
|
+
`[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
|
|
9980
|
+
formatErrorForLog(e)
|
|
9981
|
+
);
|
|
9982
|
+
}
|
|
9983
|
+
}
|
|
9984
|
+
}
|
|
9985
|
+
/**
|
|
9986
|
+
* Attach event, push, channelInfo, and guard listeners to the current
|
|
9987
|
+
* "general" client. Called from the constructor and from
|
|
9988
|
+
* {@link reconnectGeneralSocket}.
|
|
9989
|
+
*/
|
|
9990
|
+
setupGeneralClientListeners() {
|
|
9991
|
+
const client = this.client;
|
|
9992
|
+
client.on("event", (event) => {
|
|
9993
|
+
const mapped = mapToSimpleEvent(event);
|
|
9994
|
+
if (!mapped) return;
|
|
9995
|
+
this.dispatchSimpleEvent(mapped);
|
|
9996
|
+
});
|
|
9997
|
+
client.on("channelInfo", (xml) => {
|
|
9998
|
+
try {
|
|
9999
|
+
this.parseAndStoreChannelInfo(xml);
|
|
10000
|
+
} catch (e) {
|
|
10001
|
+
this.logger.warn?.(
|
|
10002
|
+
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
10003
|
+
formatErrorForLog(e)
|
|
10004
|
+
);
|
|
10005
|
+
}
|
|
10006
|
+
});
|
|
10007
|
+
client.on("push", (frame) => {
|
|
10008
|
+
const cmdId = frame.header.cmdId;
|
|
10009
|
+
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) {
|
|
10010
|
+
return;
|
|
10011
|
+
}
|
|
10012
|
+
try {
|
|
10013
|
+
if (frame.body.length === 0) return;
|
|
10014
|
+
const xml = client.tryDecryptXml(
|
|
10015
|
+
frame.body,
|
|
10016
|
+
frame.header.channelId,
|
|
10017
|
+
client.enc
|
|
10018
|
+
);
|
|
10019
|
+
if (!xml || !xml.startsWith("<?xml")) return;
|
|
10020
|
+
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
10021
|
+
} catch (e) {
|
|
10022
|
+
this.logger.debug?.(
|
|
10023
|
+
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
10024
|
+
formatErrorForLog(e)
|
|
10025
|
+
);
|
|
10026
|
+
}
|
|
10027
|
+
});
|
|
10028
|
+
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
10029
|
+
client.on("close", () => {
|
|
10030
|
+
try {
|
|
10031
|
+
void this.maybeRebootOnDisconnectStorm();
|
|
10032
|
+
} catch {
|
|
10033
|
+
}
|
|
10034
|
+
});
|
|
10035
|
+
}
|
|
10036
|
+
if (this.rebootAfterConsecutiveEconnreset > 0) {
|
|
10037
|
+
client.on("close", () => {
|
|
10038
|
+
try {
|
|
10039
|
+
void this.maybeRebootOnEconnresetStorm();
|
|
10040
|
+
} catch {
|
|
10041
|
+
}
|
|
10042
|
+
});
|
|
10043
|
+
}
|
|
10044
|
+
if (!this.sessionGuardIntervalTimer) {
|
|
10045
|
+
client.once("push", () => {
|
|
10046
|
+
void this.logActiveSessionsOnStartup();
|
|
10047
|
+
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
10048
|
+
void this.maybeRebootOnTooManySessions();
|
|
10049
|
+
}, 6e4);
|
|
10050
|
+
});
|
|
10051
|
+
}
|
|
10052
|
+
}
|
|
9582
10053
|
/**
|
|
9583
10054
|
* Cached camera UID. May be initially undefined if not provided in the constructor.
|
|
9584
10055
|
* Will be lazily populated on demand when needed (e.g. for recordings).
|
|
@@ -10519,42 +10990,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10519
10990
|
logger: this.logger,
|
|
10520
10991
|
debugConfig: generalClient.getDebugConfig?.()
|
|
10521
10992
|
});
|
|
10522
|
-
this.client.on("event", (event) => {
|
|
10523
|
-
const mapped = mapToSimpleEvent(event);
|
|
10524
|
-
if (!mapped) return;
|
|
10525
|
-
this.dispatchSimpleEvent(mapped);
|
|
10526
|
-
});
|
|
10527
|
-
this.client.on("channelInfo", (xml) => {
|
|
10528
|
-
try {
|
|
10529
|
-
this.parseAndStoreChannelInfo(xml);
|
|
10530
|
-
} catch (e) {
|
|
10531
|
-
this.logger.warn?.(
|
|
10532
|
-
"[ReolinkBaichuanApi] Error parsing channel info from push",
|
|
10533
|
-
formatErrorForLog(e)
|
|
10534
|
-
);
|
|
10535
|
-
}
|
|
10536
|
-
});
|
|
10537
|
-
this.client.on("push", (frame) => {
|
|
10538
|
-
const cmdId = frame.header.cmdId;
|
|
10539
|
-
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) {
|
|
10540
|
-
return;
|
|
10541
|
-
}
|
|
10542
|
-
try {
|
|
10543
|
-
if (frame.body.length === 0) return;
|
|
10544
|
-
const xml = this.client.tryDecryptXml(
|
|
10545
|
-
frame.body,
|
|
10546
|
-
frame.header.channelId,
|
|
10547
|
-
this.client.enc
|
|
10548
|
-
);
|
|
10549
|
-
if (!xml || !xml.startsWith("<?xml")) return;
|
|
10550
|
-
this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
|
|
10551
|
-
} catch (e) {
|
|
10552
|
-
this.logger.debug?.(
|
|
10553
|
-
"[ReolinkBaichuanApi] Error parsing settings push",
|
|
10554
|
-
formatErrorForLog(e)
|
|
10555
|
-
);
|
|
10556
|
-
}
|
|
10557
|
-
});
|
|
10558
10993
|
const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
|
|
10559
10994
|
if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
|
|
10560
10995
|
this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
|
|
@@ -10563,32 +10998,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10563
10998
|
if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
|
|
10564
10999
|
this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
|
|
10565
11000
|
}
|
|
10566
|
-
if (this.rebootAfterDisconnectionsPerMinute > 0) {
|
|
10567
|
-
this.client.on("close", () => {
|
|
10568
|
-
try {
|
|
10569
|
-
void this.maybeRebootOnDisconnectStorm();
|
|
10570
|
-
} catch {
|
|
10571
|
-
}
|
|
10572
|
-
});
|
|
10573
|
-
}
|
|
10574
11001
|
const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
|
|
10575
11002
|
if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
|
|
10576
11003
|
this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
|
|
10577
11004
|
}
|
|
10578
|
-
|
|
10579
|
-
this.client.on("close", () => {
|
|
10580
|
-
try {
|
|
10581
|
-
void this.maybeRebootOnEconnresetStorm();
|
|
10582
|
-
} catch {
|
|
10583
|
-
}
|
|
10584
|
-
});
|
|
10585
|
-
}
|
|
10586
|
-
this.client.once("push", () => {
|
|
10587
|
-
void this.logActiveSessionsOnStartup();
|
|
10588
|
-
this.sessionGuardIntervalTimer = setInterval(() => {
|
|
10589
|
-
void this.maybeRebootOnTooManySessions();
|
|
10590
|
-
}, 6e4);
|
|
10591
|
-
});
|
|
11005
|
+
this.setupGeneralClientListeners();
|
|
10592
11006
|
}
|
|
10593
11007
|
/**
|
|
10594
11008
|
* CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
|
|
@@ -11419,6 +11833,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11419
11833
|
);
|
|
11420
11834
|
}
|
|
11421
11835
|
async close(options) {
|
|
11836
|
+
if (this._closed) return;
|
|
11837
|
+
this._closed = true;
|
|
11422
11838
|
if (this.sessionGuardIntervalTimer) {
|
|
11423
11839
|
clearInterval(this.sessionGuardIntervalTimer);
|
|
11424
11840
|
this.sessionGuardIntervalTimer = void 0;
|
|
@@ -11481,7 +11897,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11481
11897
|
}
|
|
11482
11898
|
async handleSendXml400(params, frame, retry) {
|
|
11483
11899
|
const emptyBody = frame.body.length === 0;
|
|
11484
|
-
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes:
|
|
11900
|
+
const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
|
|
11485
11901
|
if (this.isSendXmlFailFast400(params, frame.body.length)) {
|
|
11486
11902
|
throw new Error(emptyBody400Msg);
|
|
11487
11903
|
}
|
|
@@ -11997,11 +12413,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11997
12413
|
* Minimal per-channel inventory for NVR-connected devices.
|
|
11998
12414
|
*
|
|
11999
12415
|
* Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
|
|
12416
|
+
*
|
|
12417
|
+
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
12418
|
+
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
12419
|
+
* no dependency on async push messages. Recommended for first-call discovery.
|
|
12420
|
+
* - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
|
|
12421
|
+
* info after login + event subscription. This push is *asynchronous*: if it has not
|
|
12422
|
+
* arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
|
|
12423
|
+
* with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
|
|
12000
12424
|
*/
|
|
12001
12425
|
async getNvrChannelsSummary(options) {
|
|
12002
|
-
const source = options?.source ?? "
|
|
12003
|
-
|
|
12004
|
-
const
|
|
12426
|
+
const source = options?.source ?? "cgi";
|
|
12427
|
+
let channels;
|
|
12428
|
+
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
12429
|
+
if (options?.channels?.length) {
|
|
12430
|
+
channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
12431
|
+
} else if (source === "cgi") {
|
|
12432
|
+
try {
|
|
12433
|
+
const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
|
|
12434
|
+
const status = channelsResponse?.[0]?.value?.status ?? [];
|
|
12435
|
+
for (const s of status) {
|
|
12436
|
+
const ch = Number(s?.channel);
|
|
12437
|
+
if (!Number.isFinite(ch)) continue;
|
|
12438
|
+
cgiStatusByChannel.set(ch, {
|
|
12439
|
+
...s.name != null ? { name: s.name } : {},
|
|
12440
|
+
...s.uid != null ? { uid: s.uid } : {},
|
|
12441
|
+
sleeping: s.sleep === 1
|
|
12442
|
+
});
|
|
12443
|
+
}
|
|
12444
|
+
channels = cgiChannels;
|
|
12445
|
+
this.logger.debug?.(
|
|
12446
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
12447
|
+
);
|
|
12448
|
+
} catch (e) {
|
|
12449
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
12450
|
+
this.logger.warn?.(
|
|
12451
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
|
|
12452
|
+
);
|
|
12453
|
+
channels = [];
|
|
12454
|
+
}
|
|
12455
|
+
} else {
|
|
12456
|
+
const pushInfo2 = this.getChannelInfoFromPushCache();
|
|
12457
|
+
channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
12458
|
+
}
|
|
12459
|
+
channels = channels.sort((a, b) => a - b);
|
|
12005
12460
|
const support = await this.getSupportInfo().catch(() => {
|
|
12006
12461
|
this.logger.error?.(
|
|
12007
12462
|
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
@@ -12031,7 +12486,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
12031
12486
|
);
|
|
12032
12487
|
}
|
|
12033
12488
|
}
|
|
12034
|
-
const cacheKey =
|
|
12489
|
+
const cacheKey = `${source}:${channels.join(",")}`;
|
|
12035
12490
|
const cached = this.nvrChannelsSummaryCache.get(cacheKey);
|
|
12036
12491
|
if (cached) {
|
|
12037
12492
|
return {
|
|
@@ -12052,8 +12507,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
12052
12507
|
} catch {
|
|
12053
12508
|
}
|
|
12054
12509
|
}
|
|
12510
|
+
const pushInfo = this.getChannelInfoFromPushCache();
|
|
12055
12511
|
const devices = channels.map((channel) => {
|
|
12056
|
-
const
|
|
12512
|
+
const pushCached = pushInfo.get(channel);
|
|
12513
|
+
const cgiStatus = cgiStatusByChannel.get(channel);
|
|
12057
12514
|
const info = infoPerChannel.get(channel);
|
|
12058
12515
|
const networkInfo = networkInfoPerChannel.get(channel);
|
|
12059
12516
|
const isBattery = isBatteryByChannel.get(channel) ?? false;
|
|
@@ -12061,6 +12518,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
12061
12518
|
const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
|
|
12062
12519
|
const normalizedModel = model ? model.trim() : void 0;
|
|
12063
12520
|
const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
|
|
12521
|
+
const name = pushCached?.name || cgiStatus?.name || "";
|
|
12522
|
+
const uid = pushCached?.uid || cgiStatus?.uid || "";
|
|
12523
|
+
const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
|
|
12064
12524
|
return {
|
|
12065
12525
|
channel,
|
|
12066
12526
|
isBattery,
|
|
@@ -12070,19 +12530,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
12070
12530
|
...networkInfo?.ip ? { ip: networkInfo.ip } : {},
|
|
12071
12531
|
...networkInfo?.mac ? { mac: networkInfo.mac } : {},
|
|
12072
12532
|
...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
|
|
12073
|
-
...
|
|
12074
|
-
...
|
|
12075
|
-
...
|
|
12076
|
-
...typeof
|
|
12077
|
-
...
|
|
12078
|
-
...
|
|
12079
|
-
...
|
|
12080
|
-
...typeof
|
|
12081
|
-
...typeof
|
|
12082
|
-
...typeof
|
|
12083
|
-
...typeof
|
|
12084
|
-
...
|
|
12085
|
-
...typeof
|
|
12533
|
+
...name ? { name } : {},
|
|
12534
|
+
...uid ? { uid } : {},
|
|
12535
|
+
...pushCached?.state ? { state: pushCached.state } : {},
|
|
12536
|
+
...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
|
|
12537
|
+
...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
|
|
12538
|
+
...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
|
|
12539
|
+
...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
|
|
12540
|
+
...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
|
|
12541
|
+
...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
|
|
12542
|
+
...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
|
|
12543
|
+
...typeof sleeping === "boolean" ? { sleeping } : {},
|
|
12544
|
+
...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
|
|
12545
|
+
...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
|
|
12086
12546
|
};
|
|
12087
12547
|
});
|
|
12088
12548
|
const result = { channels, devices };
|
|
@@ -16350,13 +16810,12 @@ ${xml}`
|
|
|
16350
16810
|
]);
|
|
16351
16811
|
const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
|
|
16352
16812
|
const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
|
|
16353
|
-
const supportItem =
|
|
16354
|
-
const capabilities =
|
|
16355
|
-
ch,
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
);
|
|
16813
|
+
const supportItem = getSupportItemForChannel(support, ch);
|
|
16814
|
+
const capabilities = computeDeviceCapabilities({
|
|
16815
|
+
channel: ch,
|
|
16816
|
+
...support != null && { support },
|
|
16817
|
+
...abilities != null && { abilities }
|
|
16818
|
+
});
|
|
16360
16819
|
const item = supportItem;
|
|
16361
16820
|
const lightType = item?.lightType;
|
|
16362
16821
|
const ledCtrl = item?.ledCtrl;
|
|
@@ -16372,6 +16831,25 @@ ${xml}`
|
|
|
16372
16831
|
});
|
|
16373
16832
|
capabilities.hasFloodlight = probed;
|
|
16374
16833
|
}
|
|
16834
|
+
let dingDongListIds;
|
|
16835
|
+
let dingDongCfgIds;
|
|
16836
|
+
let wirelessChimeError;
|
|
16837
|
+
if (capabilities.hasWirelessChime) {
|
|
16838
|
+
try {
|
|
16839
|
+
const list = await this.getDingDongList(ch);
|
|
16840
|
+
dingDongListIds = list.map((d) => d.id);
|
|
16841
|
+
const first = list[0];
|
|
16842
|
+
const fromList = first !== void 0 && first.id >= 0;
|
|
16843
|
+
if (!fromList) {
|
|
16844
|
+
const configs = await this.getDingDongCfg(ch);
|
|
16845
|
+
dingDongCfgIds = configs.map((c) => c.id);
|
|
16846
|
+
capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
|
|
16847
|
+
}
|
|
16848
|
+
} catch (e) {
|
|
16849
|
+
capabilities.hasWirelessChime = false;
|
|
16850
|
+
wirelessChimeError = e instanceof Error ? e.message : String(e);
|
|
16851
|
+
}
|
|
16852
|
+
}
|
|
16375
16853
|
const features = this.parseFeaturesFromSupport(support);
|
|
16376
16854
|
const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
|
|
16377
16855
|
const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
|
|
@@ -16408,7 +16886,10 @@ ${xml}`
|
|
|
16408
16886
|
...abilities && {
|
|
16409
16887
|
abilityMergedKeyCount: Object.keys(abilities).length
|
|
16410
16888
|
},
|
|
16411
|
-
...support?.items && { supportItemCount: support.items.length }
|
|
16889
|
+
...support?.items && { supportItemCount: support.items.length },
|
|
16890
|
+
...dingDongListIds !== void 0 && { dingDongListIds },
|
|
16891
|
+
...dingDongCfgIds !== void 0 && { dingDongCfgIds },
|
|
16892
|
+
...wirelessChimeError !== void 0 && { wirelessChimeError }
|
|
16412
16893
|
};
|
|
16413
16894
|
const result = {
|
|
16414
16895
|
capabilities,
|
|
@@ -16435,90 +16916,6 @@ ${xml}`
|
|
|
16435
16916
|
this.deviceCapabilitiesCache.clear();
|
|
16436
16917
|
}
|
|
16437
16918
|
}
|
|
16438
|
-
/**
|
|
16439
|
-
* Pick the best SupportItem for a channel.
|
|
16440
|
-
* Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
|
|
16441
|
-
*/
|
|
16442
|
-
pickBestSupportItem(support, channel) {
|
|
16443
|
-
if (!support?.items?.length) return void 0;
|
|
16444
|
-
const candidates = support.items.filter((i) => i.chnID === channel);
|
|
16445
|
-
if (!candidates.length) return void 0;
|
|
16446
|
-
const score = (item) => {
|
|
16447
|
-
const anyItem = item;
|
|
16448
|
-
let result = 0;
|
|
16449
|
-
if (anyItem.name == null) result += 100;
|
|
16450
|
-
const capabilityKeys = [
|
|
16451
|
-
"ptzType",
|
|
16452
|
-
"ptzControl",
|
|
16453
|
-
"ptzPreset",
|
|
16454
|
-
"ledCtrl",
|
|
16455
|
-
"lightType",
|
|
16456
|
-
"battery",
|
|
16457
|
-
"audioVersion",
|
|
16458
|
-
"motion",
|
|
16459
|
-
"encCtrl",
|
|
16460
|
-
"newIspCfg",
|
|
16461
|
-
"remoteAbility",
|
|
16462
|
-
"aitype",
|
|
16463
|
-
"videoClip",
|
|
16464
|
-
"snap"
|
|
16465
|
-
];
|
|
16466
|
-
for (const k of capabilityKeys) {
|
|
16467
|
-
if (anyItem[k] !== void 0) result += 3;
|
|
16468
|
-
}
|
|
16469
|
-
return result;
|
|
16470
|
-
};
|
|
16471
|
-
return candidates.sort((a, b) => score(b) - score(a))[0];
|
|
16472
|
-
}
|
|
16473
|
-
/**
|
|
16474
|
-
* Parse device capabilities from SupportInfo.
|
|
16475
|
-
* Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
|
|
16476
|
-
*/
|
|
16477
|
-
parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
|
|
16478
|
-
const truthy = (v) => {
|
|
16479
|
-
if (typeof v === "number") return v > 0;
|
|
16480
|
-
if (typeof v === "string") {
|
|
16481
|
-
const n = Number(v);
|
|
16482
|
-
return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
|
|
16483
|
-
}
|
|
16484
|
-
return Boolean(v);
|
|
16485
|
-
};
|
|
16486
|
-
const item = supportItem;
|
|
16487
|
-
const ptzMode = support?.ptzMode?.toLowerCase();
|
|
16488
|
-
const ptzType = item ? truthy(item.ptzType) : false;
|
|
16489
|
-
const ptzControl = item ? truthy(item.ptzControl) : false;
|
|
16490
|
-
const hasPtzFromItem = ptzType || ptzControl;
|
|
16491
|
-
const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
|
|
16492
|
-
const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
|
|
16493
|
-
const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
|
|
16494
|
-
const hasPresets = item ? truthy(item.ptzPreset) : false;
|
|
16495
|
-
const hasBattery = item ? truthy(item.battery) : false;
|
|
16496
|
-
const hasSiren = item ? truthy(item.audioVersion) : false;
|
|
16497
|
-
const lightType = item?.lightType;
|
|
16498
|
-
const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
|
|
16499
|
-
const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
|
|
16500
|
-
const isDoorbell = item ? truthy(item.doorbellVersion) : false;
|
|
16501
|
-
const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
|
|
16502
|
-
return {
|
|
16503
|
-
channel,
|
|
16504
|
-
...ptzMode && { ptzMode },
|
|
16505
|
-
hasPan: hasPanTilt,
|
|
16506
|
-
hasTilt: hasPanTilt,
|
|
16507
|
-
hasZoom,
|
|
16508
|
-
hasPresets,
|
|
16509
|
-
hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
|
|
16510
|
-
hasBattery,
|
|
16511
|
-
hasIntercom,
|
|
16512
|
-
hasSiren,
|
|
16513
|
-
hasFloodlight,
|
|
16514
|
-
hasPir,
|
|
16515
|
-
isDoorbell,
|
|
16516
|
-
// Autotracking: explicit flags only (autoPt or smartAI)
|
|
16517
|
-
// Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
|
|
16518
|
-
// on cameras that have PTZ and AI detection but NOT autotracking capability.
|
|
16519
|
-
hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
|
|
16520
|
-
};
|
|
16521
|
-
}
|
|
16522
16919
|
/**
|
|
16523
16920
|
* Parse support features from SupportInfo.
|
|
16524
16921
|
*/
|
|
@@ -17287,7 +17684,7 @@ ${xml}`
|
|
|
17287
17684
|
* @returns Test results for all stream types and profiles
|
|
17288
17685
|
*/
|
|
17289
17686
|
async testChannelStreams(channel, logger) {
|
|
17290
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
17687
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-FNLGCOVA.js");
|
|
17291
17688
|
return await testChannelStreams({
|
|
17292
17689
|
api: this,
|
|
17293
17690
|
channel: this.normalizeChannel(channel),
|
|
@@ -17303,7 +17700,7 @@ ${xml}`
|
|
|
17303
17700
|
* @returns Complete diagnostics for all channels and streams
|
|
17304
17701
|
*/
|
|
17305
17702
|
async collectMultifocalDiagnostics(logger) {
|
|
17306
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
17703
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-FNLGCOVA.js");
|
|
17307
17704
|
return await collectMultifocalDiagnostics({
|
|
17308
17705
|
api: this,
|
|
17309
17706
|
logger
|
|
@@ -19391,6 +19788,216 @@ ${scheduleItems}
|
|
|
19391
19788
|
const channel = 0;
|
|
19392
19789
|
return await this.getSnapshot(channel);
|
|
19393
19790
|
}
|
|
19791
|
+
// --------------------
|
|
19792
|
+
// Chime / DingDong APIs
|
|
19793
|
+
// --------------------
|
|
19794
|
+
/**
|
|
19795
|
+
* Get the list of paired wireless chime devices.
|
|
19796
|
+
* cmd_id: 484 (GetDingDongList)
|
|
19797
|
+
*
|
|
19798
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19799
|
+
* @returns Array of paired chime devices
|
|
19800
|
+
*/
|
|
19801
|
+
async getDingDongList(channel) {
|
|
19802
|
+
const ch = this.normalizeChannel(channel);
|
|
19803
|
+
const xml = await this.sendXml({
|
|
19804
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
|
|
19805
|
+
channel: ch
|
|
19806
|
+
});
|
|
19807
|
+
return parseDingDongListFromXml(xml);
|
|
19808
|
+
}
|
|
19809
|
+
/**
|
|
19810
|
+
* Get parameters (name, volume, LED state) for a specific wireless chime.
|
|
19811
|
+
* cmd_id: 485 (DingDongOpt, option getParam)
|
|
19812
|
+
*
|
|
19813
|
+
* @param chimeId - The chime device ID
|
|
19814
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19815
|
+
* @returns Chime parameters
|
|
19816
|
+
*/
|
|
19817
|
+
async getDingDongParams(chimeId, channel) {
|
|
19818
|
+
const ch = this.normalizeChannel(channel);
|
|
19819
|
+
const payloadXml = buildDingDongGetParamsXml(chimeId);
|
|
19820
|
+
const xml = await this.sendXml({
|
|
19821
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
19822
|
+
channel: ch,
|
|
19823
|
+
payloadXml
|
|
19824
|
+
});
|
|
19825
|
+
return parseDingDongParamsFromXml(xml);
|
|
19826
|
+
}
|
|
19827
|
+
/**
|
|
19828
|
+
* Set parameters (name, volume, LED state) for a specific wireless chime.
|
|
19829
|
+
* cmd_id: 485 (DingDongOpt, option setParam)
|
|
19830
|
+
*
|
|
19831
|
+
* @param chimeId - The chime device ID
|
|
19832
|
+
* @param params - Parameters to set (volLevel, ledState, name)
|
|
19833
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19834
|
+
*/
|
|
19835
|
+
async setDingDongParams(chimeId, params, channel) {
|
|
19836
|
+
const ch = this.normalizeChannel(channel);
|
|
19837
|
+
const payloadXml = buildDingDongSetParamsXml(chimeId, params);
|
|
19838
|
+
await this.sendXml({
|
|
19839
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
19840
|
+
channel: ch,
|
|
19841
|
+
payloadXml
|
|
19842
|
+
});
|
|
19843
|
+
}
|
|
19844
|
+
/**
|
|
19845
|
+
* Trigger a wireless chime to ring with a specific ringtone.
|
|
19846
|
+
* cmd_id: 485 (DingDongOpt, option ringWithMusic)
|
|
19847
|
+
*
|
|
19848
|
+
* @param chimeId - The chime device ID
|
|
19849
|
+
* @param musicId - The ringtone/music ID to play
|
|
19850
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19851
|
+
*/
|
|
19852
|
+
async ringDingDong(chimeId, musicId, channel) {
|
|
19853
|
+
const ch = this.normalizeChannel(channel);
|
|
19854
|
+
const payloadXml = buildDingDongRingXml(chimeId, musicId);
|
|
19855
|
+
await this.sendXml({
|
|
19856
|
+
cmdId: BC_CMD_ID_DING_DONG_OPT,
|
|
19857
|
+
channel: ch,
|
|
19858
|
+
payloadXml
|
|
19859
|
+
});
|
|
19860
|
+
}
|
|
19861
|
+
/**
|
|
19862
|
+
* Get the per-event alarm configuration for paired wireless chimes.
|
|
19863
|
+
* cmd_id: 486 (GetDingDongCfg)
|
|
19864
|
+
*
|
|
19865
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19866
|
+
* @returns Array of chime configurations (one per paired chime)
|
|
19867
|
+
*/
|
|
19868
|
+
async getDingDongCfg(channel) {
|
|
19869
|
+
const ch = this.normalizeChannel(channel);
|
|
19870
|
+
const xml = await this.sendXml({
|
|
19871
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
|
|
19872
|
+
channel: ch
|
|
19873
|
+
});
|
|
19874
|
+
return parseDingDongCfgFromXml(xml);
|
|
19875
|
+
}
|
|
19876
|
+
/**
|
|
19877
|
+
* Set the per-event alarm configuration for a specific wireless chime.
|
|
19878
|
+
* cmd_id: 487 (SetDingDongCfg)
|
|
19879
|
+
*
|
|
19880
|
+
* @param chimeId - The chime ring/device ID
|
|
19881
|
+
* @param eventType - Event type string (e.g. "doorbell", "package", "people")
|
|
19882
|
+
* @param state - 0 = disabled, 1 = enabled
|
|
19883
|
+
* @param musicId - Ringtone ID to use for this event type
|
|
19884
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19885
|
+
*/
|
|
19886
|
+
async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
|
|
19887
|
+
const ch = this.normalizeChannel(channel);
|
|
19888
|
+
const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
|
|
19889
|
+
await this.sendXml({
|
|
19890
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
|
|
19891
|
+
channel: ch,
|
|
19892
|
+
payloadXml
|
|
19893
|
+
});
|
|
19894
|
+
}
|
|
19895
|
+
/** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
|
|
19896
|
+
_hardwiredChimeCache = /* @__PURE__ */ new Map();
|
|
19897
|
+
/**
|
|
19898
|
+
* Get the hardwired (wired-in) chime state.
|
|
19899
|
+
* cmd_id: 483 (GetDingDongCtrl)
|
|
19900
|
+
*
|
|
19901
|
+
* Note: calling this may briefly trigger the physical chime to rattle.
|
|
19902
|
+
*
|
|
19903
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19904
|
+
* @returns Hardwired chime state (type, enabled, time)
|
|
19905
|
+
*/
|
|
19906
|
+
async getHardwiredChime(channel) {
|
|
19907
|
+
const ch = this.normalizeChannel(channel);
|
|
19908
|
+
const payloadXml = buildGetDingDongCtrlXml();
|
|
19909
|
+
const xml = await this.sendXml({
|
|
19910
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
19911
|
+
channel: ch,
|
|
19912
|
+
payloadXml
|
|
19913
|
+
});
|
|
19914
|
+
const state = parseHardwiredChimeFromXml(xml);
|
|
19915
|
+
this._hardwiredChimeCache.set(ch, state);
|
|
19916
|
+
return state;
|
|
19917
|
+
}
|
|
19918
|
+
/**
|
|
19919
|
+
* Set the hardwired (wired-in) chime state.
|
|
19920
|
+
* cmd_id: 483 (SetDingDongCtrl)
|
|
19921
|
+
*
|
|
19922
|
+
* Uses the cached state from a previous getHardwiredChime call to fill in
|
|
19923
|
+
* missing type/time fields, avoiding a double round-trip on every set.
|
|
19924
|
+
* Falls back to fetching if no cache is available.
|
|
19925
|
+
*
|
|
19926
|
+
* @param params - Chime configuration (type, enabled, time)
|
|
19927
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19928
|
+
*/
|
|
19929
|
+
async setHardwiredChime(params, channel) {
|
|
19930
|
+
const ch = this.normalizeChannel(channel);
|
|
19931
|
+
let current = this._hardwiredChimeCache.get(ch);
|
|
19932
|
+
if (!current) {
|
|
19933
|
+
current = await this.getHardwiredChime(ch);
|
|
19934
|
+
}
|
|
19935
|
+
const chimeType = params.type ?? current.type;
|
|
19936
|
+
const enabled = params.enabled ? 1 : 0;
|
|
19937
|
+
const time = params.time ?? current.time;
|
|
19938
|
+
const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
|
|
19939
|
+
const xml = await this.sendXml({
|
|
19940
|
+
cmdId: BC_CMD_ID_DING_DONG_CTRL,
|
|
19941
|
+
channel: ch,
|
|
19942
|
+
payloadXml
|
|
19943
|
+
});
|
|
19944
|
+
const newState = parseHardwiredChimeFromXml(xml);
|
|
19945
|
+
this._hardwiredChimeCache.set(ch, newState);
|
|
19946
|
+
return newState;
|
|
19947
|
+
}
|
|
19948
|
+
/**
|
|
19949
|
+
* Play an audio file on the doorbell / chime device.
|
|
19950
|
+
* cmd_id: 349 (QuickReplyPlay)
|
|
19951
|
+
*
|
|
19952
|
+
* @param fileId - The audio file ID to play
|
|
19953
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19954
|
+
*/
|
|
19955
|
+
async quickReplyPlay(fileId, channel) {
|
|
19956
|
+
const ch = this.normalizeChannel(channel);
|
|
19957
|
+
const payloadXml = buildQuickReplyPlayXml(ch, fileId);
|
|
19958
|
+
await this.sendXml({
|
|
19959
|
+
cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
19960
|
+
channel: ch,
|
|
19961
|
+
payloadXml
|
|
19962
|
+
});
|
|
19963
|
+
}
|
|
19964
|
+
/**
|
|
19965
|
+
* Get the silent mode state of a paired wireless chime.
|
|
19966
|
+
* cmd_id: 609 (GetDingDongSilent)
|
|
19967
|
+
*
|
|
19968
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
19969
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19970
|
+
* @returns Wireless chime silent state (time=0 means active/not silenced)
|
|
19971
|
+
*/
|
|
19972
|
+
async getDingDongSilent(chimeId, channel) {
|
|
19973
|
+
const ch = this.normalizeChannel(channel);
|
|
19974
|
+
const payloadXml = buildGetDingDongSilentXml(chimeId);
|
|
19975
|
+
const xml = await this.sendXml({
|
|
19976
|
+
cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
19977
|
+
channel: ch,
|
|
19978
|
+
payloadXml
|
|
19979
|
+
});
|
|
19980
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
19981
|
+
}
|
|
19982
|
+
/**
|
|
19983
|
+
* Set the silent mode of a paired wireless chime.
|
|
19984
|
+
* cmd_id: 610 (SetDingDongSilent)
|
|
19985
|
+
*
|
|
19986
|
+
* @param chimeId - The wireless chime device ID (from getDingDongList)
|
|
19987
|
+
* @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
|
|
19988
|
+
* @param channel - Channel number (0-based, default 0)
|
|
19989
|
+
* @returns Updated wireless chime silent state
|
|
19990
|
+
*/
|
|
19991
|
+
async setDingDongSilent(chimeId, time, channel) {
|
|
19992
|
+
const ch = this.normalizeChannel(channel);
|
|
19993
|
+
const payloadXml = buildSetDingDongSilentXml(chimeId, time);
|
|
19994
|
+
const xml = await this.sendXml({
|
|
19995
|
+
cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
19996
|
+
channel: ch,
|
|
19997
|
+
payloadXml
|
|
19998
|
+
});
|
|
19999
|
+
return parseWirelessChimeSilentFromXml(xml, chimeId);
|
|
20000
|
+
}
|
|
19394
20001
|
};
|
|
19395
20002
|
|
|
19396
20003
|
// src/reolink/discovery.ts
|
|
@@ -20388,6 +20995,7 @@ export {
|
|
|
20388
20995
|
flattenAbilitiesForChannel,
|
|
20389
20996
|
abilitiesHasAny,
|
|
20390
20997
|
parseSupportXml,
|
|
20998
|
+
getSupportItemForChannel,
|
|
20391
20999
|
computeDeviceCapabilities,
|
|
20392
21000
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
20393
21001
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
@@ -20406,4 +21014,4 @@ export {
|
|
|
20406
21014
|
isTcpFailureThatShouldFallbackToUdp,
|
|
20407
21015
|
autoDetectDeviceType
|
|
20408
21016
|
};
|
|
20409
|
-
//# sourceMappingURL=chunk-
|
|
21017
|
+
//# sourceMappingURL=chunk-RWYEGEWG.js.map
|