@apocaliss92/nodelink-js 0.4.7 → 0.4.10
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/dist/{DiagnosticsTools-UMN4C7SY.js → DiagnosticsTools-RNIDFEJK.js} +2 -2
- package/dist/{chunk-TR3V5FTO.js → chunk-EDLMKBG2.js} +226 -3
- package/dist/chunk-EDLMKBG2.js.map +1 -0
- package/dist/{chunk-GKLOJJ34.js → chunk-HGQ53FB3.js} +1023 -341
- package/dist/chunk-HGQ53FB3.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +1172 -312
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +1407 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +989 -6
- package/dist/index.d.ts +1050 -5
- package/dist/index.js +232 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GKLOJJ34.js.map +0 -1
- package/dist/chunk-TR3V5FTO.js.map +0 -1
- /package/dist/{DiagnosticsTools-UMN4C7SY.js.map → DiagnosticsTools-RNIDFEJK.js.map} +0 -0
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
BC_CMD_ID_GET_AUDIO_ALARM,
|
|
32
32
|
BC_CMD_ID_GET_AUDIO_CFG,
|
|
33
33
|
BC_CMD_ID_GET_AUDIO_TASK,
|
|
34
|
+
BC_CMD_ID_GET_AUTO_FOCUS,
|
|
34
35
|
BC_CMD_ID_GET_BATTERY_INFO,
|
|
35
36
|
BC_CMD_ID_GET_BATTERY_INFO_LIST,
|
|
36
37
|
BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
BC_CMD_ID_GET_DING_DONG_LIST,
|
|
40
41
|
BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
41
42
|
BC_CMD_ID_GET_EMAIL_TASK,
|
|
43
|
+
BC_CMD_ID_GET_ENC,
|
|
42
44
|
BC_CMD_ID_GET_FTP_TASK,
|
|
43
45
|
BC_CMD_ID_GET_HDD_INFO_LIST,
|
|
44
46
|
BC_CMD_ID_GET_KIT_AP_CFG,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
BC_CMD_ID_GET_ONLINE_USER_LIST,
|
|
48
50
|
BC_CMD_ID_GET_OSD_DATETIME,
|
|
49
51
|
BC_CMD_ID_GET_PIR_INFO,
|
|
52
|
+
BC_CMD_ID_GET_PRIVACY_MASK,
|
|
50
53
|
BC_CMD_ID_GET_PTZ_POSITION,
|
|
51
54
|
BC_CMD_ID_GET_PTZ_PRESET,
|
|
52
55
|
BC_CMD_ID_GET_RECORD,
|
|
@@ -76,11 +79,19 @@ import {
|
|
|
76
79
|
BC_CMD_ID_QUICK_REPLY_PLAY,
|
|
77
80
|
BC_CMD_ID_SET_AI_ALARM,
|
|
78
81
|
BC_CMD_ID_SET_AI_CFG,
|
|
82
|
+
BC_CMD_ID_SET_AI_DENOISE,
|
|
83
|
+
BC_CMD_ID_SET_AUDIO_CFG,
|
|
79
84
|
BC_CMD_ID_SET_AUDIO_TASK,
|
|
85
|
+
BC_CMD_ID_SET_AUTO_FOCUS,
|
|
86
|
+
BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
|
|
80
87
|
BC_CMD_ID_SET_DING_DONG_CFG,
|
|
81
88
|
BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
89
|
+
BC_CMD_ID_SET_ENC,
|
|
90
|
+
BC_CMD_ID_SET_LED_STATE,
|
|
82
91
|
BC_CMD_ID_SET_MOTION_ALARM,
|
|
83
92
|
BC_CMD_ID_SET_PIR_INFO,
|
|
93
|
+
BC_CMD_ID_SET_PRIVACY_MASK,
|
|
94
|
+
BC_CMD_ID_SET_VIDEO_INPUT,
|
|
84
95
|
BC_CMD_ID_SET_WHITE_LED_STATE,
|
|
85
96
|
BC_CMD_ID_SET_WHITE_LED_TASK,
|
|
86
97
|
BC_CMD_ID_SET_ZOOM_FOCUS,
|
|
@@ -102,6 +113,8 @@ import {
|
|
|
102
113
|
__require,
|
|
103
114
|
aesDecrypt,
|
|
104
115
|
aesEncrypt,
|
|
116
|
+
applyStreamPatch,
|
|
117
|
+
applyXmlTagPatch,
|
|
105
118
|
bcDecrypt,
|
|
106
119
|
bcEncrypt,
|
|
107
120
|
bcHeaderHasPayloadOffset,
|
|
@@ -127,6 +140,7 @@ import {
|
|
|
127
140
|
convertToAnnexB2,
|
|
128
141
|
debugLog,
|
|
129
142
|
deriveAesKey,
|
|
143
|
+
ensureXmlHeader,
|
|
130
144
|
eventTraceLog,
|
|
131
145
|
extractPpsFromAnnexB,
|
|
132
146
|
extractSpsFromAnnexB,
|
|
@@ -134,8 +148,11 @@ import {
|
|
|
134
148
|
getXmlText,
|
|
135
149
|
isH265Irap,
|
|
136
150
|
md5StrModern,
|
|
151
|
+
normalizeDayNightMode,
|
|
137
152
|
normalizeDebugOptions,
|
|
153
|
+
normalizeOpenClose,
|
|
138
154
|
parseRecordingFileName,
|
|
155
|
+
patchNestedTag,
|
|
139
156
|
recordingsTraceLog,
|
|
140
157
|
runAllDiagnosticsConsecutively,
|
|
141
158
|
runMultifocalDiagnosticsConsecutively,
|
|
@@ -144,7 +161,7 @@ import {
|
|
|
144
161
|
talkTraceLog,
|
|
145
162
|
traceLog,
|
|
146
163
|
xmlEscape
|
|
147
|
-
} from "./chunk-
|
|
164
|
+
} from "./chunk-EDLMKBG2.js";
|
|
148
165
|
|
|
149
166
|
// src/protocol/framing.ts
|
|
150
167
|
function encodeHeader(h) {
|
|
@@ -5325,19 +5342,34 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
5325
5342
|
}
|
|
5326
5343
|
});
|
|
5327
5344
|
streamStarted = true;
|
|
5328
|
-
|
|
5345
|
+
const signal = options?.signal;
|
|
5346
|
+
while (!closed && !signal?.aborted) {
|
|
5329
5347
|
if (frameQueue.length > 0) {
|
|
5330
5348
|
const frame = frameQueue.shift();
|
|
5331
5349
|
yield frame;
|
|
5332
5350
|
} else {
|
|
5333
5351
|
await new Promise((resolve) => {
|
|
5334
5352
|
frameResolve = resolve;
|
|
5335
|
-
setTimeout(() => {
|
|
5353
|
+
const timer = setTimeout(() => {
|
|
5336
5354
|
if (frameResolve === resolve) {
|
|
5337
5355
|
frameResolve = null;
|
|
5338
5356
|
resolve();
|
|
5339
5357
|
}
|
|
5340
5358
|
}, 1e3);
|
|
5359
|
+
if (signal) {
|
|
5360
|
+
const onAbort = () => {
|
|
5361
|
+
clearTimeout(timer);
|
|
5362
|
+
if (frameResolve === resolve) frameResolve = null;
|
|
5363
|
+
resolve();
|
|
5364
|
+
};
|
|
5365
|
+
if (signal.aborted) {
|
|
5366
|
+
clearTimeout(timer);
|
|
5367
|
+
frameResolve = null;
|
|
5368
|
+
resolve();
|
|
5369
|
+
} else {
|
|
5370
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5341
5373
|
});
|
|
5342
5374
|
}
|
|
5343
5375
|
}
|
|
@@ -5513,13 +5545,14 @@ var NativeStreamFanout = class {
|
|
|
5513
5545
|
source = null;
|
|
5514
5546
|
running = false;
|
|
5515
5547
|
pumpPromise = null;
|
|
5548
|
+
abort = new AbortController();
|
|
5516
5549
|
constructor(opts) {
|
|
5517
5550
|
this.opts = opts;
|
|
5518
5551
|
}
|
|
5519
5552
|
start() {
|
|
5520
5553
|
if (this.running) return;
|
|
5521
5554
|
this.running = true;
|
|
5522
|
-
this.source = this.opts.createSource();
|
|
5555
|
+
this.source = this.opts.createSource(this.abort.signal);
|
|
5523
5556
|
this.pumpPromise = (async () => {
|
|
5524
5557
|
try {
|
|
5525
5558
|
for await (const frame of this.source) {
|
|
@@ -5565,6 +5598,7 @@ var NativeStreamFanout = class {
|
|
|
5565
5598
|
this.source = null;
|
|
5566
5599
|
for (const q of this.queues.values()) q.close();
|
|
5567
5600
|
this.queues.clear();
|
|
5601
|
+
this.abort.abort();
|
|
5568
5602
|
try {
|
|
5569
5603
|
await src?.return(void 0);
|
|
5570
5604
|
} catch {
|
|
@@ -5603,9 +5637,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5603
5637
|
requireAuth;
|
|
5604
5638
|
authNonces = /* @__PURE__ */ new Map();
|
|
5605
5639
|
// Track nonces per client
|
|
5606
|
-
AUTH_REALM
|
|
5640
|
+
AUTH_REALM;
|
|
5607
5641
|
NONCE_TIMEOUT_MS = 3e5;
|
|
5608
5642
|
// 5 minutes
|
|
5643
|
+
lazyMetadata;
|
|
5609
5644
|
// Client tracking
|
|
5610
5645
|
connectedClients = /* @__PURE__ */ new Set();
|
|
5611
5646
|
// Set of client IDs (IP:port)
|
|
@@ -5617,8 +5652,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5617
5652
|
// Track all client resources for cleanup
|
|
5618
5653
|
clientResources = /* @__PURE__ */ new Map();
|
|
5619
5654
|
isRtspDebugEnabled() {
|
|
5620
|
-
|
|
5621
|
-
|
|
5655
|
+
try {
|
|
5656
|
+
if (this.api.isClosed) {
|
|
5657
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
5658
|
+
}
|
|
5659
|
+
const dbg = this.api.client.getDebugConfig();
|
|
5660
|
+
return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
5661
|
+
} catch {
|
|
5662
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
5663
|
+
}
|
|
5622
5664
|
}
|
|
5623
5665
|
rtspDebugLog(message) {
|
|
5624
5666
|
if (!this.isRtspDebugEnabled()) return;
|
|
@@ -5640,10 +5682,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5640
5682
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
5641
5683
|
nativeFanout = null;
|
|
5642
5684
|
noClientAutoStopTimer;
|
|
5685
|
+
/** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
|
|
5686
|
+
noFrameDeadlineTimer;
|
|
5643
5687
|
/** After last RTSP client; 0 = never auto-stop native stream. */
|
|
5644
5688
|
nativeStreamIdleStopMs;
|
|
5645
5689
|
/** Primed-but-no-PLAY timeout; 0 = disabled. */
|
|
5646
5690
|
nativeStreamPrimeIdleStopMs;
|
|
5691
|
+
/**
|
|
5692
|
+
* Max time to wait for the first camera frame after stream start.
|
|
5693
|
+
* If no frames arrive within this window, the native stream is stopped
|
|
5694
|
+
* (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
|
|
5695
|
+
* firing and waking the camera when no real viewer is watching.
|
|
5696
|
+
* 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
|
|
5697
|
+
*/
|
|
5698
|
+
nativeStreamNoFrameDeadlineMs;
|
|
5647
5699
|
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
5648
5700
|
// When a new client connects while the stream is already running it does not need
|
|
5649
5701
|
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
@@ -5769,14 +5821,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5769
5821
|
this.logger = options.logger ?? console;
|
|
5770
5822
|
this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
|
|
5771
5823
|
this.deviceId = options.deviceId;
|
|
5772
|
-
this.externalListener = options.externalListener ?? false;
|
|
5824
|
+
this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
|
|
5773
5825
|
this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
|
|
5774
5826
|
this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
|
|
5775
|
-
this.
|
|
5827
|
+
this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
|
|
5828
|
+
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
5829
|
+
username: c.username,
|
|
5830
|
+
...c.password !== void 0 ? { password: c.password } : {},
|
|
5831
|
+
...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
|
|
5832
|
+
}));
|
|
5776
5833
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
5834
|
+
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
5835
|
+
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
5777
5836
|
const transport = this.api.client.getTransport();
|
|
5778
5837
|
this.flow = createRtspFlow(transport, "H264");
|
|
5779
5838
|
}
|
|
5839
|
+
/** Number of currently connected RTSP clients. */
|
|
5840
|
+
get clientCount() {
|
|
5841
|
+
return this.connectedClients.size;
|
|
5842
|
+
}
|
|
5780
5843
|
// --- Authentication helpers ---
|
|
5781
5844
|
/**
|
|
5782
5845
|
* Generate a new nonce for Digest authentication
|
|
@@ -5837,9 +5900,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5837
5900
|
this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
|
|
5838
5901
|
return false;
|
|
5839
5902
|
}
|
|
5903
|
+
if (realm !== this.AUTH_REALM) {
|
|
5904
|
+
this.rtspDebugLog(
|
|
5905
|
+
`Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
|
|
5906
|
+
);
|
|
5907
|
+
return false;
|
|
5908
|
+
}
|
|
5840
5909
|
for (const cred of this.authCredentials) {
|
|
5841
5910
|
if (username !== cred.username) continue;
|
|
5842
|
-
const ha1 = this.md5(`${cred.username}:${
|
|
5911
|
+
const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
|
|
5912
|
+
if (!ha1) continue;
|
|
5843
5913
|
const ha2 = this.md5(`${method}:${authUri || uri}`);
|
|
5844
5914
|
const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
|
|
5845
5915
|
if (response === expectedResponse) {
|
|
@@ -5868,6 +5938,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5868
5938
|
this.noClientAutoStopTimer = void 0;
|
|
5869
5939
|
}
|
|
5870
5940
|
}
|
|
5941
|
+
clearNoFrameDeadlineTimer() {
|
|
5942
|
+
if (this.noFrameDeadlineTimer) {
|
|
5943
|
+
clearTimeout(this.noFrameDeadlineTimer);
|
|
5944
|
+
this.noFrameDeadlineTimer = void 0;
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5871
5947
|
setFlowVideoType(videoType, reason) {
|
|
5872
5948
|
if (this.flow.videoType === videoType) return;
|
|
5873
5949
|
const transport = this.api.client.getTransport();
|
|
@@ -5882,25 +5958,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5882
5958
|
if (this.active) {
|
|
5883
5959
|
throw new Error("RTSP server is already active");
|
|
5884
5960
|
}
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
if (stream) {
|
|
5889
|
-
this.streamMetadata = {
|
|
5890
|
-
frameRate: stream.frameRate || 25,
|
|
5891
|
-
width: stream.width,
|
|
5892
|
-
height: stream.height
|
|
5893
|
-
};
|
|
5894
|
-
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
5895
|
-
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
5896
|
-
this.setFlowVideoType(metaVideoType, "metadata");
|
|
5897
|
-
}
|
|
5898
|
-
} catch (error) {
|
|
5899
|
-
this.logger.warn(
|
|
5900
|
-
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
5961
|
+
if (this.lazyMetadata) {
|
|
5962
|
+
this.logger.info(
|
|
5963
|
+
`[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
|
|
5901
5964
|
);
|
|
5902
|
-
|
|
5903
|
-
|
|
5965
|
+
} else {
|
|
5966
|
+
try {
|
|
5967
|
+
const metadata = await this.api.getStreamMetadata(this.channel);
|
|
5968
|
+
const stream = metadata.streams.find((s) => s.profile === this.profile);
|
|
5969
|
+
if (stream) {
|
|
5970
|
+
this.streamMetadata = {
|
|
5971
|
+
frameRate: stream.frameRate || 25,
|
|
5972
|
+
width: stream.width,
|
|
5973
|
+
height: stream.height
|
|
5974
|
+
};
|
|
5975
|
+
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
5976
|
+
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
5977
|
+
this.setFlowVideoType(metaVideoType, "metadata");
|
|
5978
|
+
}
|
|
5979
|
+
} catch (error) {
|
|
5980
|
+
this.logger.warn(
|
|
5981
|
+
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
5982
|
+
);
|
|
5983
|
+
this.streamMetadata = { frameRate: 25 };
|
|
5984
|
+
this.setFlowVideoType("H264", "metadata unavailable");
|
|
5985
|
+
}
|
|
5904
5986
|
}
|
|
5905
5987
|
if (!this.externalListener) {
|
|
5906
5988
|
this.clientConnectionServer = net2.createServer((socket) => {
|
|
@@ -5941,6 +6023,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5941
6023
|
}
|
|
5942
6024
|
this.handleRtspConnection(socket, initialBuffer);
|
|
5943
6025
|
}
|
|
6026
|
+
/**
|
|
6027
|
+
* Inject an already-accepted client socket from a multiplexer
|
|
6028
|
+
* (e.g. `LocalRtspMux`) that owns the listening port.
|
|
6029
|
+
*
|
|
6030
|
+
* The mux reads the first RTSP request line to determine the target path,
|
|
6031
|
+
* then hands the socket over. Any bytes already consumed during routing
|
|
6032
|
+
* are replayed back onto the socket via `unshift()` so the RTSP parser in
|
|
6033
|
+
* `handleRtspConnection` sees the complete original request.
|
|
6034
|
+
*
|
|
6035
|
+
* @param socket - Client TCP socket, already accepted by the mux.
|
|
6036
|
+
* @param preReadData - Bytes the mux has already pulled off the socket
|
|
6037
|
+
* while parsing the request line. Replayed via `socket.unshift()`
|
|
6038
|
+
* before any further reads.
|
|
6039
|
+
*/
|
|
6040
|
+
injectSocket(socket, preReadData) {
|
|
6041
|
+
if (!this.active) {
|
|
6042
|
+
socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
|
|
6043
|
+
return;
|
|
6044
|
+
}
|
|
6045
|
+
if (preReadData && preReadData.length > 0) {
|
|
6046
|
+
socket.unshift(preReadData);
|
|
6047
|
+
}
|
|
6048
|
+
this.handleRtspConnection(socket);
|
|
6049
|
+
}
|
|
5944
6050
|
/**
|
|
5945
6051
|
* Handle RTSP connection from a client.
|
|
5946
6052
|
*/
|
|
@@ -6105,6 +6211,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6105
6211
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
6106
6212
|
});
|
|
6107
6213
|
} else if (method === "DESCRIBE") {
|
|
6214
|
+
if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
|
|
6215
|
+
void this.api.ensureConnected().catch(() => {
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
6108
6218
|
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
6109
6219
|
try {
|
|
6110
6220
|
if (!this.nativeStreamActive) {
|
|
@@ -6142,6 +6252,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6142
6252
|
}
|
|
6143
6253
|
}
|
|
6144
6254
|
}
|
|
6255
|
+
if (!this.hasAudio && this.firstAudioPromise) {
|
|
6256
|
+
const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
|
|
6257
|
+
const audioPrimingStart = Date.now();
|
|
6258
|
+
try {
|
|
6259
|
+
await Promise.race([
|
|
6260
|
+
this.firstAudioPromise,
|
|
6261
|
+
new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
|
|
6262
|
+
]);
|
|
6263
|
+
} catch {
|
|
6264
|
+
}
|
|
6265
|
+
const audioPrimingElapsed = Date.now() - audioPrimingStart;
|
|
6266
|
+
if (this.hasAudio) {
|
|
6267
|
+
this.logger.info(
|
|
6268
|
+
`[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
|
|
6269
|
+
);
|
|
6270
|
+
} else {
|
|
6271
|
+
this.logger.info(
|
|
6272
|
+
`[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
|
|
6273
|
+
);
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6145
6276
|
{
|
|
6146
6277
|
const { fmtp, hasParamSets } = this.flow.getFmtp();
|
|
6147
6278
|
const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
|
|
@@ -6150,12 +6281,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6150
6281
|
);
|
|
6151
6282
|
}
|
|
6152
6283
|
const sdp = this.generateSdp();
|
|
6284
|
+
const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
|
|
6153
6285
|
sendResponse(
|
|
6154
6286
|
200,
|
|
6155
6287
|
"OK",
|
|
6156
6288
|
{
|
|
6157
6289
|
"Content-Type": "application/sdp",
|
|
6158
|
-
"Content-Base": `rtsp://${
|
|
6290
|
+
"Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
|
|
6159
6291
|
},
|
|
6160
6292
|
sdp
|
|
6161
6293
|
);
|
|
@@ -6178,7 +6310,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6178
6310
|
this.emit("client", clientId);
|
|
6179
6311
|
this.clearNoClientAutoStopTimer();
|
|
6180
6312
|
if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
|
|
6181
|
-
|
|
6313
|
+
void this.startNativeStream();
|
|
6182
6314
|
}
|
|
6183
6315
|
const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
|
|
6184
6316
|
const transport = (transportMatch?.[1] ?? "").trim();
|
|
@@ -6312,12 +6444,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6312
6444
|
}
|
|
6313
6445
|
}
|
|
6314
6446
|
};
|
|
6447
|
+
const runProcessBuffer = () => {
|
|
6448
|
+
processBuffer().catch((err) => {
|
|
6449
|
+
this.logger.debug(
|
|
6450
|
+
`[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
|
|
6451
|
+
);
|
|
6452
|
+
try {
|
|
6453
|
+
socket.destroy();
|
|
6454
|
+
} catch {
|
|
6455
|
+
}
|
|
6456
|
+
});
|
|
6457
|
+
};
|
|
6315
6458
|
socket.on("data", (data) => {
|
|
6316
6459
|
buffer = Buffer.concat([buffer, data]);
|
|
6317
|
-
|
|
6460
|
+
runProcessBuffer();
|
|
6318
6461
|
});
|
|
6319
6462
|
if (buffer.includes("\r\n\r\n")) {
|
|
6320
|
-
|
|
6463
|
+
runProcessBuffer();
|
|
6321
6464
|
}
|
|
6322
6465
|
}
|
|
6323
6466
|
/**
|
|
@@ -7185,6 +7328,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7185
7328
|
if (this.nativeStreamActive) {
|
|
7186
7329
|
return;
|
|
7187
7330
|
}
|
|
7331
|
+
if (!this.api.isReady) {
|
|
7332
|
+
if (this.api.isClosed) {
|
|
7333
|
+
this.logger.warn?.(
|
|
7334
|
+
`[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
|
|
7335
|
+
);
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7338
|
+
try {
|
|
7339
|
+
this.logger.info?.(
|
|
7340
|
+
`[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
|
|
7341
|
+
);
|
|
7342
|
+
await this.api.ensureConnected();
|
|
7343
|
+
} catch (e) {
|
|
7344
|
+
this.logger.warn?.(
|
|
7345
|
+
`[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
|
|
7346
|
+
);
|
|
7347
|
+
return;
|
|
7348
|
+
}
|
|
7349
|
+
}
|
|
7188
7350
|
this.nativeStreamActive = true;
|
|
7189
7351
|
this.firstFrameReceived = false;
|
|
7190
7352
|
this.firstAudioDetected = false;
|
|
@@ -7219,13 +7381,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7219
7381
|
await this.flow.startKeepAlive(this.api);
|
|
7220
7382
|
this.nativeFanout = new NativeStreamFanout({
|
|
7221
7383
|
maxQueueItems: 200,
|
|
7222
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
7384
|
+
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
7223
7385
|
variant: this.variant,
|
|
7224
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
7386
|
+
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
7387
|
+
signal
|
|
7225
7388
|
}),
|
|
7226
7389
|
onFrame: (frame) => {
|
|
7227
7390
|
if (frame.audio) {
|
|
7228
|
-
if (!this.hasAudio &&
|
|
7391
|
+
if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
|
|
7229
7392
|
const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
|
|
7230
7393
|
if (info) {
|
|
7231
7394
|
this.hasAudio = true;
|
|
@@ -7274,6 +7437,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7274
7437
|
onEnd: () => {
|
|
7275
7438
|
if (!this.nativeStreamActive) return;
|
|
7276
7439
|
this.nativeStreamActive = false;
|
|
7440
|
+
this.clearNoFrameDeadlineTimer();
|
|
7441
|
+
const hadFrames = this.firstFrameReceived;
|
|
7277
7442
|
this.firstFrameReceived = false;
|
|
7278
7443
|
this.firstFramePromise = null;
|
|
7279
7444
|
this.firstFrameResolve = null;
|
|
@@ -7298,7 +7463,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7298
7463
|
} catch {
|
|
7299
7464
|
}
|
|
7300
7465
|
}
|
|
7301
|
-
if (this.connectedClients.size > 0) {
|
|
7466
|
+
if (this.connectedClients.size > 0 && hadFrames) {
|
|
7302
7467
|
this.logger.info(
|
|
7303
7468
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
7304
7469
|
);
|
|
@@ -7310,6 +7475,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7310
7475
|
}
|
|
7311
7476
|
});
|
|
7312
7477
|
this.nativeFanout.start();
|
|
7478
|
+
this.clearNoFrameDeadlineTimer();
|
|
7479
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
7480
|
+
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
7481
|
+
this.noFrameDeadlineTimer = void 0;
|
|
7482
|
+
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
7483
|
+
this.logger.info(
|
|
7484
|
+
`[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
|
|
7485
|
+
);
|
|
7486
|
+
void this.stopNativeStream();
|
|
7487
|
+
}
|
|
7488
|
+
}, this.nativeStreamNoFrameDeadlineMs);
|
|
7489
|
+
this.noFrameDeadlineTimer?.unref?.();
|
|
7490
|
+
}
|
|
7313
7491
|
this.clearNoClientAutoStopTimer();
|
|
7314
7492
|
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
7315
7493
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
@@ -7326,6 +7504,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7326
7504
|
markFirstFrameReceived() {
|
|
7327
7505
|
if (!this.firstFrameReceived && this.firstFrameResolve) {
|
|
7328
7506
|
this.firstFrameReceived = true;
|
|
7507
|
+
this.clearNoFrameDeadlineTimer();
|
|
7329
7508
|
this.rtspDebugLog(
|
|
7330
7509
|
`First frame received from camera for profile ${this.profile}`
|
|
7331
7510
|
);
|
|
@@ -7352,6 +7531,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7352
7531
|
);
|
|
7353
7532
|
this.flow.stopKeepAlive();
|
|
7354
7533
|
this.clearNoClientAutoStopTimer();
|
|
7534
|
+
this.clearNoFrameDeadlineTimer();
|
|
7355
7535
|
this.nativeStreamActive = false;
|
|
7356
7536
|
this.firstFrameReceived = false;
|
|
7357
7537
|
this.firstFramePromise = null;
|
|
@@ -7561,6 +7741,249 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7561
7741
|
}
|
|
7562
7742
|
};
|
|
7563
7743
|
|
|
7744
|
+
// src/baichuan/stream/MpegTsMuxer.ts
|
|
7745
|
+
var TS_PACKET_SIZE = 188;
|
|
7746
|
+
var TS_SYNC_BYTE = 71;
|
|
7747
|
+
var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
|
|
7748
|
+
var PID_PAT = 0;
|
|
7749
|
+
var PID_PMT = 4096;
|
|
7750
|
+
var PID_VIDEO = 256;
|
|
7751
|
+
var PID_AUDIO = 257;
|
|
7752
|
+
var STREAM_TYPE_H264 = 27;
|
|
7753
|
+
var STREAM_TYPE_H265 = 36;
|
|
7754
|
+
var STREAM_TYPE_AAC = 15;
|
|
7755
|
+
var PES_STREAM_ID_VIDEO = 224;
|
|
7756
|
+
var PES_STREAM_ID_AUDIO = 192;
|
|
7757
|
+
var PAT_PMT_INTERVAL = 40;
|
|
7758
|
+
function crc32Mpeg(data) {
|
|
7759
|
+
let crc = 4294967295;
|
|
7760
|
+
for (let i = 0; i < data.length; i++) {
|
|
7761
|
+
crc ^= data[i] << 24;
|
|
7762
|
+
for (let j = 0; j < 8; j++) {
|
|
7763
|
+
if (crc & 2147483648) {
|
|
7764
|
+
crc = (crc << 1 ^ 79764919) >>> 0;
|
|
7765
|
+
} else {
|
|
7766
|
+
crc = crc << 1 >>> 0;
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
return crc >>> 0;
|
|
7771
|
+
}
|
|
7772
|
+
function usToPts(us) {
|
|
7773
|
+
return Math.floor(us * 90 / 1e3) & 8589934591;
|
|
7774
|
+
}
|
|
7775
|
+
function encodePts(buf, offset, pts, prefix) {
|
|
7776
|
+
buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
|
|
7777
|
+
buf[offset + 1] = pts >>> 22 & 255;
|
|
7778
|
+
buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
|
|
7779
|
+
buf[offset + 3] = pts >>> 7 & 255;
|
|
7780
|
+
buf[offset + 4] = (pts & 127) << 1 | 1;
|
|
7781
|
+
}
|
|
7782
|
+
function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
|
|
7783
|
+
buf[0] = TS_SYNC_BYTE;
|
|
7784
|
+
buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
|
|
7785
|
+
buf[2] = pid & 255;
|
|
7786
|
+
buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
|
|
7787
|
+
}
|
|
7788
|
+
function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
|
|
7789
|
+
const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
|
|
7790
|
+
const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
|
|
7791
|
+
let pesOffset = 0;
|
|
7792
|
+
let outOffset = 0;
|
|
7793
|
+
let isFirst = true;
|
|
7794
|
+
while (pesOffset < pesData.length) {
|
|
7795
|
+
const remaining = pesData.length - pesOffset;
|
|
7796
|
+
const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
|
|
7797
|
+
outOffset += TS_PACKET_SIZE;
|
|
7798
|
+
if (remaining >= TS_PAYLOAD_SIZE) {
|
|
7799
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
|
|
7800
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
7801
|
+
pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
|
|
7802
|
+
pesOffset += TS_PAYLOAD_SIZE;
|
|
7803
|
+
} else {
|
|
7804
|
+
const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
|
|
7805
|
+
if (paddingNeeded === 1) {
|
|
7806
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
7807
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
7808
|
+
packet[4] = 0;
|
|
7809
|
+
pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
|
|
7810
|
+
} else {
|
|
7811
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
7812
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
7813
|
+
const adaptLen = paddingNeeded - 1;
|
|
7814
|
+
packet[4] = adaptLen;
|
|
7815
|
+
packet[5] = isFirst && isKeyframe ? 64 : 0;
|
|
7816
|
+
packet.fill(255, 6, 4 + paddingNeeded);
|
|
7817
|
+
pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
|
|
7818
|
+
}
|
|
7819
|
+
pesOffset += remaining;
|
|
7820
|
+
}
|
|
7821
|
+
isFirst = false;
|
|
7822
|
+
}
|
|
7823
|
+
return out;
|
|
7824
|
+
}
|
|
7825
|
+
function buildPat(cc) {
|
|
7826
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7827
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
7828
|
+
pkt[1] = 64 | PID_PAT >> 8 & 31;
|
|
7829
|
+
pkt[2] = PID_PAT & 255;
|
|
7830
|
+
pkt[3] = 16 | cc & 15;
|
|
7831
|
+
pkt[4] = 0;
|
|
7832
|
+
const sectionStart = 5;
|
|
7833
|
+
let i = sectionStart;
|
|
7834
|
+
pkt[i++] = 0;
|
|
7835
|
+
pkt[i++] = 176;
|
|
7836
|
+
pkt[i++] = 13;
|
|
7837
|
+
pkt[i++] = 0;
|
|
7838
|
+
pkt[i++] = 1;
|
|
7839
|
+
pkt[i++] = 193;
|
|
7840
|
+
pkt[i++] = 0;
|
|
7841
|
+
pkt[i++] = 0;
|
|
7842
|
+
pkt[i++] = 0;
|
|
7843
|
+
pkt[i++] = 1;
|
|
7844
|
+
pkt[i++] = 224 | PID_PMT >> 8 & 31;
|
|
7845
|
+
pkt[i++] = PID_PMT & 255;
|
|
7846
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
7847
|
+
pkt.writeUInt32BE(crc, i);
|
|
7848
|
+
return pkt;
|
|
7849
|
+
}
|
|
7850
|
+
function buildPmt(videoStreamType, includeAudio, cc) {
|
|
7851
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7852
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
7853
|
+
pkt[1] = 64 | PID_PMT >> 8 & 31;
|
|
7854
|
+
pkt[2] = PID_PMT & 255;
|
|
7855
|
+
pkt[3] = 16 | cc & 15;
|
|
7856
|
+
pkt[4] = 0;
|
|
7857
|
+
const sectionStart = 5;
|
|
7858
|
+
let i = sectionStart;
|
|
7859
|
+
pkt[i++] = 2;
|
|
7860
|
+
pkt[i++] = 176;
|
|
7861
|
+
const sectionLenPos = i;
|
|
7862
|
+
i += 1;
|
|
7863
|
+
pkt[i++] = 0;
|
|
7864
|
+
pkt[i++] = 1;
|
|
7865
|
+
pkt[i++] = 193;
|
|
7866
|
+
pkt[i++] = 0;
|
|
7867
|
+
pkt[i++] = 0;
|
|
7868
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
7869
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
7870
|
+
pkt[i++] = 240;
|
|
7871
|
+
pkt[i++] = 0;
|
|
7872
|
+
pkt[i++] = videoStreamType;
|
|
7873
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
7874
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
7875
|
+
pkt[i++] = 240;
|
|
7876
|
+
pkt[i++] = 0;
|
|
7877
|
+
if (includeAudio) {
|
|
7878
|
+
pkt[i++] = STREAM_TYPE_AAC;
|
|
7879
|
+
pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
|
|
7880
|
+
pkt[i++] = PID_AUDIO & 255;
|
|
7881
|
+
pkt[i++] = 240;
|
|
7882
|
+
pkt[i++] = 0;
|
|
7883
|
+
}
|
|
7884
|
+
const sectionLen = i - sectionStart - 3 + 4;
|
|
7885
|
+
pkt[sectionLenPos] = sectionLen;
|
|
7886
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
7887
|
+
pkt.writeUInt32BE(crc, i);
|
|
7888
|
+
return pkt;
|
|
7889
|
+
}
|
|
7890
|
+
function buildVideoPes(annexBData, ptsUs, isKeyframe) {
|
|
7891
|
+
const pts = usToPts(ptsUs);
|
|
7892
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
7893
|
+
pesHeader[0] = 0;
|
|
7894
|
+
pesHeader[1] = 0;
|
|
7895
|
+
pesHeader[2] = 1;
|
|
7896
|
+
pesHeader[3] = PES_STREAM_ID_VIDEO;
|
|
7897
|
+
pesHeader[4] = 0;
|
|
7898
|
+
pesHeader[5] = 0;
|
|
7899
|
+
pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
|
|
7900
|
+
pesHeader[7] = 128;
|
|
7901
|
+
pesHeader[8] = 5;
|
|
7902
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
7903
|
+
return Buffer.concat([pesHeader, annexBData]);
|
|
7904
|
+
}
|
|
7905
|
+
function buildAudioPes(adtsData, ptsUs) {
|
|
7906
|
+
const pts = usToPts(ptsUs);
|
|
7907
|
+
const pesPayloadLen = 8 + adtsData.length;
|
|
7908
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
7909
|
+
pesHeader[0] = 0;
|
|
7910
|
+
pesHeader[1] = 0;
|
|
7911
|
+
pesHeader[2] = 1;
|
|
7912
|
+
pesHeader[3] = PES_STREAM_ID_AUDIO;
|
|
7913
|
+
pesHeader[4] = pesPayloadLen >> 8 & 255;
|
|
7914
|
+
pesHeader[5] = pesPayloadLen & 255;
|
|
7915
|
+
pesHeader[6] = 128;
|
|
7916
|
+
pesHeader[7] = 128;
|
|
7917
|
+
pesHeader[8] = 5;
|
|
7918
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
7919
|
+
return Buffer.concat([pesHeader, adtsData]);
|
|
7920
|
+
}
|
|
7921
|
+
var MpegTsMuxer = class {
|
|
7922
|
+
videoStreamType;
|
|
7923
|
+
includeAudio;
|
|
7924
|
+
// Per-instance continuity counters (4-bit, wrap at 16)
|
|
7925
|
+
patCc = 0;
|
|
7926
|
+
pmtCc = 0;
|
|
7927
|
+
videoCc = 0;
|
|
7928
|
+
audioCc = 0;
|
|
7929
|
+
framesSinceTableSend = 0;
|
|
7930
|
+
tablesSent = false;
|
|
7931
|
+
constructor(options) {
|
|
7932
|
+
this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
7933
|
+
this.includeAudio = options.includeAudio ?? true;
|
|
7934
|
+
}
|
|
7935
|
+
/**
|
|
7936
|
+
* Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
|
|
7937
|
+
* PAT and PMT are emitted before keyframes and periodically.
|
|
7938
|
+
*
|
|
7939
|
+
* @param annexBData - Annex-B video data (with start codes)
|
|
7940
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
7941
|
+
* @param isKeyframe - Whether this is an IDR / IRAP frame
|
|
7942
|
+
*/
|
|
7943
|
+
muxVideo(annexBData, ptsUs, isKeyframe) {
|
|
7944
|
+
const chunks = [];
|
|
7945
|
+
const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
|
|
7946
|
+
if (needTables) {
|
|
7947
|
+
chunks.push(buildPat(this.patCc));
|
|
7948
|
+
this.patCc = this.patCc + 1 & 15;
|
|
7949
|
+
chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
|
|
7950
|
+
this.pmtCc = this.pmtCc + 1 & 15;
|
|
7951
|
+
this.tablesSent = true;
|
|
7952
|
+
this.framesSinceTableSend = 0;
|
|
7953
|
+
}
|
|
7954
|
+
this.framesSinceTableSend++;
|
|
7955
|
+
const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
|
|
7956
|
+
const ccRef = { cc: this.videoCc };
|
|
7957
|
+
chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
|
|
7958
|
+
this.videoCc = ccRef.cc;
|
|
7959
|
+
return Buffer.concat(chunks);
|
|
7960
|
+
}
|
|
7961
|
+
/**
|
|
7962
|
+
* Mux an audio frame (ADTS AAC) into MPEG-TS packets.
|
|
7963
|
+
* Returns an empty Buffer when includeAudio is false.
|
|
7964
|
+
*
|
|
7965
|
+
* @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
|
|
7966
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
7967
|
+
*/
|
|
7968
|
+
muxAudio(adtsData, ptsUs) {
|
|
7969
|
+
if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
|
|
7970
|
+
const pes = buildAudioPes(adtsData, ptsUs);
|
|
7971
|
+
const ccRef = { cc: this.audioCc };
|
|
7972
|
+
const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
|
|
7973
|
+
this.audioCc = ccRef.cc;
|
|
7974
|
+
return result;
|
|
7975
|
+
}
|
|
7976
|
+
/** Reset all continuity counters and table state (e.g. after stream restart). */
|
|
7977
|
+
reset() {
|
|
7978
|
+
this.patCc = 0;
|
|
7979
|
+
this.pmtCc = 0;
|
|
7980
|
+
this.videoCc = 0;
|
|
7981
|
+
this.audioCc = 0;
|
|
7982
|
+
this.framesSinceTableSend = 0;
|
|
7983
|
+
this.tablesSent = false;
|
|
7984
|
+
}
|
|
7985
|
+
};
|
|
7986
|
+
|
|
7564
7987
|
// src/reolink/baichuan/capabilities.ts
|
|
7565
7988
|
function toNumberOrUndefined(value) {
|
|
7566
7989
|
if (value == null) return void 0;
|
|
@@ -7774,214 +8197,59 @@ function xmlIndicatesFloodlight(xml) {
|
|
|
7774
8197
|
return false;
|
|
7775
8198
|
}
|
|
7776
8199
|
|
|
8200
|
+
// src/reolink/baichuan/utils/sleepInference.ts
|
|
8201
|
+
function decideSleepInferenceTransition(input) {
|
|
8202
|
+
const { inferred, committed, pending, hysteresisPolls } = input;
|
|
8203
|
+
if (committed === void 0) {
|
|
8204
|
+
return {
|
|
8205
|
+
emit: inferred === "sleeping" ? "sleeping" : null,
|
|
8206
|
+
nextCommitted: inferred,
|
|
8207
|
+
nextPending: void 0
|
|
8208
|
+
};
|
|
8209
|
+
}
|
|
8210
|
+
if (inferred === committed) {
|
|
8211
|
+
return {
|
|
8212
|
+
emit: null,
|
|
8213
|
+
nextCommitted: committed,
|
|
8214
|
+
nextPending: void 0
|
|
8215
|
+
};
|
|
8216
|
+
}
|
|
8217
|
+
const effectivePolls = Math.max(1, hysteresisPolls);
|
|
8218
|
+
if (!pending || pending.state !== inferred) {
|
|
8219
|
+
if (effectivePolls <= 1) {
|
|
8220
|
+
return {
|
|
8221
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
8222
|
+
nextCommitted: inferred,
|
|
8223
|
+
nextPending: void 0
|
|
8224
|
+
};
|
|
8225
|
+
}
|
|
8226
|
+
return {
|
|
8227
|
+
emit: null,
|
|
8228
|
+
nextCommitted: committed,
|
|
8229
|
+
nextPending: { state: inferred, count: 1 }
|
|
8230
|
+
};
|
|
8231
|
+
}
|
|
8232
|
+
const nextCount = pending.count + 1;
|
|
8233
|
+
if (nextCount < effectivePolls) {
|
|
8234
|
+
return {
|
|
8235
|
+
emit: null,
|
|
8236
|
+
nextCommitted: committed,
|
|
8237
|
+
nextPending: { state: inferred, count: nextCount }
|
|
8238
|
+
};
|
|
8239
|
+
}
|
|
8240
|
+
return {
|
|
8241
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
8242
|
+
nextCommitted: inferred,
|
|
8243
|
+
nextPending: void 0
|
|
8244
|
+
};
|
|
8245
|
+
}
|
|
8246
|
+
|
|
7777
8247
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
7778
8248
|
import { spawn as spawn2 } from "child_process";
|
|
7779
8249
|
import { mkdir } from "fs/promises";
|
|
7780
8250
|
import { dirname } from "path";
|
|
7781
8251
|
import { PassThrough } from "stream";
|
|
7782
8252
|
|
|
7783
|
-
// src/baichuan/stream/MpegTsMuxer.ts
|
|
7784
|
-
var TS_PACKET_SIZE = 188;
|
|
7785
|
-
var TS_SYNC_BYTE = 71;
|
|
7786
|
-
var PAT_PID = 0;
|
|
7787
|
-
var PMT_PID = 4096;
|
|
7788
|
-
var VIDEO_PID = 256;
|
|
7789
|
-
var STREAM_TYPE_H264 = 27;
|
|
7790
|
-
var STREAM_TYPE_H265 = 36;
|
|
7791
|
-
var patCc = 0;
|
|
7792
|
-
var pmtCc = 0;
|
|
7793
|
-
var videoCc = 0;
|
|
7794
|
-
function createPat() {
|
|
7795
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7796
|
-
packet[0] = TS_SYNC_BYTE;
|
|
7797
|
-
packet[1] = 64 | PAT_PID >> 8 & 31;
|
|
7798
|
-
packet[2] = PAT_PID & 255;
|
|
7799
|
-
packet[3] = 16 | patCc & 15;
|
|
7800
|
-
patCc = patCc + 1 & 15;
|
|
7801
|
-
packet[4] = 0;
|
|
7802
|
-
let idx = 5;
|
|
7803
|
-
packet[idx++] = 0;
|
|
7804
|
-
packet[idx++] = 176;
|
|
7805
|
-
packet[idx++] = 13;
|
|
7806
|
-
packet[idx++] = 0;
|
|
7807
|
-
packet[idx++] = 1;
|
|
7808
|
-
packet[idx++] = 193;
|
|
7809
|
-
packet[idx++] = 0;
|
|
7810
|
-
packet[idx++] = 0;
|
|
7811
|
-
packet[idx++] = 0;
|
|
7812
|
-
packet[idx++] = 1;
|
|
7813
|
-
packet[idx++] = 224 | PMT_PID >> 8 & 31;
|
|
7814
|
-
packet[idx++] = PMT_PID & 255;
|
|
7815
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
7816
|
-
packet.writeUInt32BE(crc, idx);
|
|
7817
|
-
return packet;
|
|
7818
|
-
}
|
|
7819
|
-
function createPmt(streamType) {
|
|
7820
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7821
|
-
packet[0] = TS_SYNC_BYTE;
|
|
7822
|
-
packet[1] = 64 | PMT_PID >> 8 & 31;
|
|
7823
|
-
packet[2] = PMT_PID & 255;
|
|
7824
|
-
packet[3] = 16 | pmtCc & 15;
|
|
7825
|
-
pmtCc = pmtCc + 1 & 15;
|
|
7826
|
-
packet[4] = 0;
|
|
7827
|
-
let idx = 5;
|
|
7828
|
-
packet[idx++] = 2;
|
|
7829
|
-
packet[idx++] = 176;
|
|
7830
|
-
packet[idx++] = 18;
|
|
7831
|
-
packet[idx++] = 0;
|
|
7832
|
-
packet[idx++] = 1;
|
|
7833
|
-
packet[idx++] = 193;
|
|
7834
|
-
packet[idx++] = 0;
|
|
7835
|
-
packet[idx++] = 0;
|
|
7836
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
7837
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
7838
|
-
packet[idx++] = 240;
|
|
7839
|
-
packet[idx++] = 0;
|
|
7840
|
-
packet[idx++] = streamType;
|
|
7841
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
7842
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
7843
|
-
packet[idx++] = 240;
|
|
7844
|
-
packet[idx++] = 0;
|
|
7845
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
7846
|
-
packet.writeUInt32BE(crc, idx);
|
|
7847
|
-
return packet;
|
|
7848
|
-
}
|
|
7849
|
-
function createVideoPes(data, pts, isKeyframe) {
|
|
7850
|
-
const packets = [];
|
|
7851
|
-
const pts90k = Math.floor(pts * 9e4 / 1e6);
|
|
7852
|
-
const pesHeaderLen = 14;
|
|
7853
|
-
const pesHeader = Buffer.alloc(pesHeaderLen);
|
|
7854
|
-
let idx = 0;
|
|
7855
|
-
pesHeader[idx++] = 0;
|
|
7856
|
-
pesHeader[idx++] = 0;
|
|
7857
|
-
pesHeader[idx++] = 1;
|
|
7858
|
-
pesHeader[idx++] = 224;
|
|
7859
|
-
pesHeader[idx++] = 0;
|
|
7860
|
-
pesHeader[idx++] = 0;
|
|
7861
|
-
pesHeader[idx++] = 128;
|
|
7862
|
-
pesHeader[idx++] = 128;
|
|
7863
|
-
pesHeader[idx++] = 5;
|
|
7864
|
-
pesHeader[idx++] = 33 | pts90k >> 29 & 14;
|
|
7865
|
-
pesHeader[idx++] = pts90k >> 22 & 255;
|
|
7866
|
-
pesHeader[idx++] = 1 | pts90k >> 14 & 254;
|
|
7867
|
-
pesHeader[idx++] = pts90k >> 7 & 255;
|
|
7868
|
-
pesHeader[idx++] = 1 | pts90k << 1 & 254;
|
|
7869
|
-
const pesData = Buffer.concat([pesHeader, data]);
|
|
7870
|
-
let pesOffset = 0;
|
|
7871
|
-
let isFirst = true;
|
|
7872
|
-
while (pesOffset < pesData.length) {
|
|
7873
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7874
|
-
let pktIdx = 0;
|
|
7875
|
-
packet[pktIdx++] = TS_SYNC_BYTE;
|
|
7876
|
-
packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
|
|
7877
|
-
packet[pktIdx++] = VIDEO_PID & 255;
|
|
7878
|
-
const remaining = pesData.length - pesOffset;
|
|
7879
|
-
const maxPayload = TS_PACKET_SIZE - 4;
|
|
7880
|
-
if (remaining >= maxPayload) {
|
|
7881
|
-
packet[pktIdx++] = 16 | videoCc & 15;
|
|
7882
|
-
videoCc = videoCc + 1 & 15;
|
|
7883
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
|
|
7884
|
-
pesOffset += maxPayload;
|
|
7885
|
-
} else {
|
|
7886
|
-
const adaptLen = maxPayload - remaining - 1;
|
|
7887
|
-
if (adaptLen < 0) {
|
|
7888
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
7889
|
-
videoCc = videoCc + 1 & 15;
|
|
7890
|
-
packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
|
|
7891
|
-
if (isFirst && isKeyframe) {
|
|
7892
|
-
packet[pktIdx++] = 64;
|
|
7893
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
7894
|
-
packet[i] = 255;
|
|
7895
|
-
}
|
|
7896
|
-
} else {
|
|
7897
|
-
packet[pktIdx++] = 0;
|
|
7898
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
7899
|
-
packet[i] = 255;
|
|
7900
|
-
}
|
|
7901
|
-
}
|
|
7902
|
-
pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
|
|
7903
|
-
pesOffset += remaining;
|
|
7904
|
-
} else {
|
|
7905
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
7906
|
-
videoCc = videoCc + 1 & 15;
|
|
7907
|
-
if (adaptLen === 0) {
|
|
7908
|
-
packet[pktIdx++] = 0;
|
|
7909
|
-
} else {
|
|
7910
|
-
packet[pktIdx++] = adaptLen;
|
|
7911
|
-
if (isFirst && isKeyframe) {
|
|
7912
|
-
packet[pktIdx++] = 64;
|
|
7913
|
-
} else {
|
|
7914
|
-
packet[pktIdx++] = 0;
|
|
7915
|
-
}
|
|
7916
|
-
for (let i = 0; i < adaptLen - 1; i++) {
|
|
7917
|
-
packet[pktIdx++] = 255;
|
|
7918
|
-
}
|
|
7919
|
-
}
|
|
7920
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
|
|
7921
|
-
pesOffset += remaining;
|
|
7922
|
-
}
|
|
7923
|
-
}
|
|
7924
|
-
packets.push(packet);
|
|
7925
|
-
isFirst = false;
|
|
7926
|
-
}
|
|
7927
|
-
return packets;
|
|
7928
|
-
}
|
|
7929
|
-
function crc32Mpeg(data) {
|
|
7930
|
-
let crc = 4294967295;
|
|
7931
|
-
for (let i = 0; i < data.length; i++) {
|
|
7932
|
-
crc ^= data[i] << 24;
|
|
7933
|
-
for (let j = 0; j < 8; j++) {
|
|
7934
|
-
if (crc & 2147483648) {
|
|
7935
|
-
crc = (crc << 1 ^ 79764919) >>> 0;
|
|
7936
|
-
} else {
|
|
7937
|
-
crc = crc << 1 >>> 0;
|
|
7938
|
-
}
|
|
7939
|
-
}
|
|
7940
|
-
}
|
|
7941
|
-
return crc >>> 0;
|
|
7942
|
-
}
|
|
7943
|
-
var MpegTsMuxer = class {
|
|
7944
|
-
streamType;
|
|
7945
|
-
patSent = false;
|
|
7946
|
-
pmtSent = false;
|
|
7947
|
-
patPmtInterval = 0;
|
|
7948
|
-
patPmtIntervalMax = 40;
|
|
7949
|
-
// Send PAT/PMT every ~40 frames
|
|
7950
|
-
constructor(options) {
|
|
7951
|
-
this.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
7952
|
-
}
|
|
7953
|
-
/**
|
|
7954
|
-
* Reset continuity counters (call when starting a new stream).
|
|
7955
|
-
*/
|
|
7956
|
-
static resetCounters() {
|
|
7957
|
-
patCc = 0;
|
|
7958
|
-
pmtCc = 0;
|
|
7959
|
-
videoCc = 0;
|
|
7960
|
-
}
|
|
7961
|
-
/**
|
|
7962
|
-
* Mux a video frame into MPEG-TS packets.
|
|
7963
|
-
*
|
|
7964
|
-
* @param data - Annex-B video data (with start codes)
|
|
7965
|
-
* @param microseconds - Frame timestamp in microseconds
|
|
7966
|
-
* @param isKeyframe - Whether this is a keyframe
|
|
7967
|
-
* @returns Buffer containing all TS packets for this frame
|
|
7968
|
-
*/
|
|
7969
|
-
mux(data, microseconds, isKeyframe) {
|
|
7970
|
-
const packets = [];
|
|
7971
|
-
if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
|
|
7972
|
-
packets.push(createPat());
|
|
7973
|
-
packets.push(createPmt(this.streamType));
|
|
7974
|
-
this.patSent = true;
|
|
7975
|
-
this.pmtSent = true;
|
|
7976
|
-
this.patPmtInterval = 0;
|
|
7977
|
-
}
|
|
7978
|
-
this.patPmtInterval++;
|
|
7979
|
-
const videoPackets = createVideoPes(data, microseconds, isKeyframe);
|
|
7980
|
-
packets.push(...videoPackets);
|
|
7981
|
-
return Buffer.concat(packets);
|
|
7982
|
-
}
|
|
7983
|
-
};
|
|
7984
|
-
|
|
7985
8253
|
// src/reolink/baichuan/utils/xml.ts
|
|
7986
8254
|
import { XMLParser } from "fast-xml-parser";
|
|
7987
8255
|
var parser = new XMLParser({
|
|
@@ -10437,7 +10705,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10437
10705
|
statePollingInterval;
|
|
10438
10706
|
udpSleepInferenceInterval;
|
|
10439
10707
|
udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
10708
|
+
/**
|
|
10709
|
+
* Per-channel pending sleep-state candidate for hysteresis.
|
|
10710
|
+
* When the inference flips to a new state we require N consecutive polls
|
|
10711
|
+
* of that same state before committing it — this filters out transient
|
|
10712
|
+
* flapping caused by non-waking traffic drifting in/out of the 10 s
|
|
10713
|
+
* getSleepStatus() observation window during stream teardown.
|
|
10714
|
+
*/
|
|
10715
|
+
udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
10440
10716
|
udpSleepInferenceIntervalMs = 2e3;
|
|
10717
|
+
/** Consecutive inference polls required to commit a new sleeping/awake state. */
|
|
10718
|
+
udpSleepInferenceHysteresisPolls = 2;
|
|
10441
10719
|
lastMotionState;
|
|
10442
10720
|
lastAiState;
|
|
10443
10721
|
aiStatePollingDisabled = false;
|
|
@@ -10904,6 +11182,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10904
11182
|
*/
|
|
10905
11183
|
attachD2cDiscListener(client) {
|
|
10906
11184
|
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
11185
|
+
client.on("error", () => {
|
|
11186
|
+
});
|
|
10907
11187
|
}
|
|
10908
11188
|
/**
|
|
10909
11189
|
* Acquire a socket from the pool by tag.
|
|
@@ -11022,6 +11302,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11022
11302
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
11023
11303
|
const newClient = new BaichuanClient(clientOpts);
|
|
11024
11304
|
this.attachD2cDiscListener(newClient);
|
|
11305
|
+
newClient.on("error", (err) => {
|
|
11306
|
+
log?.debug?.(
|
|
11307
|
+
`[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
|
|
11308
|
+
);
|
|
11309
|
+
});
|
|
11025
11310
|
await newClient.login();
|
|
11026
11311
|
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
11027
11312
|
if (existingCooldown) {
|
|
@@ -11537,6 +11822,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11537
11822
|
* Only counts sessions from our own IP address.
|
|
11538
11823
|
*/
|
|
11539
11824
|
async maybeRebootOnTooManySessions() {
|
|
11825
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
11540
11826
|
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
11541
11827
|
if (this.sessionGuardRebootInFlight) return;
|
|
11542
11828
|
const cooldownMs = 10 * 6e4;
|
|
@@ -11978,6 +12264,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11978
12264
|
}
|
|
11979
12265
|
async renewSimpleEventSubscription() {
|
|
11980
12266
|
if (this.simpleEventListeners.size === 0) return;
|
|
12267
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
11981
12268
|
if (this.simpleEventResubscribeInFlight)
|
|
11982
12269
|
return await this.simpleEventResubscribeInFlight;
|
|
11983
12270
|
this.simpleEventResubscribeInFlight = (async () => {
|
|
@@ -15745,23 +16032,32 @@ ${stderr}`)
|
|
|
15745
16032
|
return;
|
|
15746
16033
|
}
|
|
15747
16034
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
16035
|
+
if (!this.client.isSocketConnected?.()) {
|
|
16036
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
16037
|
+
return;
|
|
16038
|
+
}
|
|
15748
16039
|
const status = this.getSleepStatus({ channel });
|
|
15749
16040
|
if (status.state === "unknown") return;
|
|
15750
|
-
const
|
|
15751
|
-
this.
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
16041
|
+
const committed = this.udpLastInferredSleepStateByChannel.get(channel);
|
|
16042
|
+
const pending = this.udpPendingSleepStateByChannel.get(channel);
|
|
16043
|
+
const decision = decideSleepInferenceTransition({
|
|
16044
|
+
inferred: status.state,
|
|
16045
|
+
committed,
|
|
16046
|
+
pending,
|
|
16047
|
+
hysteresisPolls: this.udpSleepInferenceHysteresisPolls
|
|
16048
|
+
});
|
|
16049
|
+
this.udpLastInferredSleepStateByChannel.set(
|
|
16050
|
+
channel,
|
|
16051
|
+
decision.nextCommitted
|
|
16052
|
+
);
|
|
16053
|
+
if (decision.nextPending === void 0) {
|
|
16054
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
16055
|
+
} else {
|
|
16056
|
+
this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
|
|
15761
16057
|
}
|
|
15762
|
-
if (
|
|
16058
|
+
if (decision.emit) {
|
|
15763
16059
|
this.dispatchSimpleEvent({
|
|
15764
|
-
type:
|
|
16060
|
+
type: decision.emit,
|
|
15765
16061
|
channel,
|
|
15766
16062
|
timestamp: Date.now()
|
|
15767
16063
|
});
|
|
@@ -15784,6 +16080,7 @@ ${stderr}`)
|
|
|
15784
16080
|
this.udpSleepInferenceInterval = void 0;
|
|
15785
16081
|
}
|
|
15786
16082
|
this.udpLastInferredSleepStateByChannel.clear();
|
|
16083
|
+
this.udpPendingSleepStateByChannel.clear();
|
|
15787
16084
|
}
|
|
15788
16085
|
/**
|
|
15789
16086
|
* GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
|
|
@@ -18180,7 +18477,7 @@ ${xml}`
|
|
|
18180
18477
|
* @returns Test results for all stream types and profiles
|
|
18181
18478
|
*/
|
|
18182
18479
|
async testChannelStreams(channel, logger) {
|
|
18183
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
18480
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-RNIDFEJK.js");
|
|
18184
18481
|
return await testChannelStreams({
|
|
18185
18482
|
api: this,
|
|
18186
18483
|
channel: this.normalizeChannel(channel),
|
|
@@ -18196,7 +18493,7 @@ ${xml}`
|
|
|
18196
18493
|
* @returns Complete diagnostics for all channels and streams
|
|
18197
18494
|
*/
|
|
18198
18495
|
async collectMultifocalDiagnostics(logger) {
|
|
18199
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
18496
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-RNIDFEJK.js");
|
|
18200
18497
|
return await collectMultifocalDiagnostics({
|
|
18201
18498
|
api: this,
|
|
18202
18499
|
logger
|
|
@@ -18214,6 +18511,373 @@ ${xml}`
|
|
|
18214
18511
|
await this.cgiApi.login();
|
|
18215
18512
|
return await this.cgiApi.getAllChannelsEvents(options);
|
|
18216
18513
|
}
|
|
18514
|
+
// ====================================================================
|
|
18515
|
+
// Native Baichuan tunable-settings setters
|
|
18516
|
+
//
|
|
18517
|
+
// Replace the CGI passthroughs above with on-wire Baichuan binary
|
|
18518
|
+
// calls. Mirrors the @http_cmd-decorated methods in reolink_aio's
|
|
18519
|
+
// baichuan.py — every command has a documented `cmd_id` (read) and
|
|
18520
|
+
// `cmd_id` (write) pair. The pattern is:
|
|
18521
|
+
//
|
|
18522
|
+
// 1. read XML via `sendXml({ cmdId: GET, channel })`
|
|
18523
|
+
// 2. patch fields via regex (camera firmware is XML-strict; using
|
|
18524
|
+
// the parser would force us to rebuild the document and risk
|
|
18525
|
+
// losing unmodified attributes / element order).
|
|
18526
|
+
// 3. write back via `sendXml({ cmdId: SET, channel, payloadXml })`
|
|
18527
|
+
//
|
|
18528
|
+
// All getters parse via `parseXmlFragmentToJson` so the consumer gets
|
|
18529
|
+
// a clean JSON object instead of XML.
|
|
18530
|
+
// ====================================================================
|
|
18531
|
+
/**
|
|
18532
|
+
* GetEnc via Baichuan (cmdId=56). Returns the `<Compression>` block:
|
|
18533
|
+
* per-stream `mainStream` / `subStream` / `thirdStream` with `audio`
|
|
18534
|
+
* flag, `width`, `height`, `frame` (NOT `frameRate`), `bitRate`,
|
|
18535
|
+
* `videoEncType` (0=h264, 1=h265), `encoderProfile`, `gop`. Mirrors
|
|
18536
|
+
* reolink_aio's `GetEnc` — note the wire payload wraps everything
|
|
18537
|
+
* in `Compression`, not `Enc`.
|
|
18538
|
+
*/
|
|
18539
|
+
async getEnc(channel, options) {
|
|
18540
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
18541
|
+
cmdId: BC_CMD_ID_GET_ENC,
|
|
18542
|
+
...channel != null ? { channel } : {},
|
|
18543
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18544
|
+
});
|
|
18545
|
+
return parseXmlFragmentToJson(xml);
|
|
18546
|
+
}
|
|
18547
|
+
/**
|
|
18548
|
+
* SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
|
|
18549
|
+
* unspecified fields. Mirrors reolink_aio's `SetEnc`.
|
|
18550
|
+
*
|
|
18551
|
+
* @param channel - Channel number (0-based)
|
|
18552
|
+
* @param patch - Fields to update on `mainStream` and/or `subStream`,
|
|
18553
|
+
* plus a top-level `audio` toggle (0/1). Pass only what you want
|
|
18554
|
+
* to change.
|
|
18555
|
+
*/
|
|
18556
|
+
async setEnc(channel, patch, options) {
|
|
18557
|
+
const ch = this.normalizeChannel(channel);
|
|
18558
|
+
let xml = await this.sendXml({
|
|
18559
|
+
cmdId: BC_CMD_ID_GET_ENC,
|
|
18560
|
+
channel: ch,
|
|
18561
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18562
|
+
});
|
|
18563
|
+
if (patch.audio !== void 0) {
|
|
18564
|
+
xml = xml.replace(
|
|
18565
|
+
/<audio>[^<]*<\/audio>/g,
|
|
18566
|
+
`<audio>${patch.audio}</audio>`
|
|
18567
|
+
);
|
|
18568
|
+
}
|
|
18569
|
+
xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
|
|
18570
|
+
xml = applyStreamPatch(xml, "subStream", patch.subStream);
|
|
18571
|
+
await this.sendXml({
|
|
18572
|
+
cmdId: BC_CMD_ID_SET_ENC,
|
|
18573
|
+
channel: ch,
|
|
18574
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18575
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18576
|
+
});
|
|
18577
|
+
}
|
|
18578
|
+
/**
|
|
18579
|
+
* SetImage via Baichuan (cmdId=25, read via cmdId=26). Patches the
|
|
18580
|
+
* `<VideoInput>` block: bright / contrast / saturation / hue /
|
|
18581
|
+
* sharpen. Mirrors reolink_aio's `SetImage`.
|
|
18582
|
+
*/
|
|
18583
|
+
async setImage(channel, patch, options) {
|
|
18584
|
+
const ch = this.normalizeChannel(channel);
|
|
18585
|
+
let xml = await this.sendXml({
|
|
18586
|
+
cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
|
|
18587
|
+
channel: ch,
|
|
18588
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18589
|
+
});
|
|
18590
|
+
xml = applyXmlTagPatch(xml, "bright", patch.bright);
|
|
18591
|
+
xml = applyXmlTagPatch(xml, "contrast", patch.contrast);
|
|
18592
|
+
xml = applyXmlTagPatch(xml, "saturation", patch.saturation);
|
|
18593
|
+
xml = applyXmlTagPatch(xml, "hue", patch.hue);
|
|
18594
|
+
xml = applyXmlTagPatch(xml, "sharpen", patch.sharpen);
|
|
18595
|
+
await this.sendXml({
|
|
18596
|
+
cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
|
|
18597
|
+
channel: ch,
|
|
18598
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18599
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18600
|
+
});
|
|
18601
|
+
}
|
|
18602
|
+
/**
|
|
18603
|
+
* SetIsp via Baichuan (cmdId=25 for image side, cmdId=297 for
|
|
18604
|
+
* dayNightThreshold). Patches the `<InputAdvanceCfg>` block:
|
|
18605
|
+
* `DayNight/mode`, `Exposure/mode`, `binning_mode`, `hdrSwitch`.
|
|
18606
|
+
* Mirrors reolink_aio's `SetIsp`.
|
|
18607
|
+
*
|
|
18608
|
+
* @param channel - Channel number (0-based)
|
|
18609
|
+
* @param patch - Fields to update. `dayNight` accepts the camera's
|
|
18610
|
+
* raw enum (`color`, `auto`, `blackAndWhite`, …) — pass it as the
|
|
18611
|
+
* camera reports it (PascalCase / dotted forms get normalized
|
|
18612
|
+
* server-side).
|
|
18613
|
+
*/
|
|
18614
|
+
async setIsp(channel, patch, options) {
|
|
18615
|
+
const ch = this.normalizeChannel(channel);
|
|
18616
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
18617
|
+
const wantsImageWrite = patch.dayNight !== void 0 || patch.exposure !== void 0 || patch.binningMode !== void 0 || patch.hdr !== void 0;
|
|
18618
|
+
if (wantsImageWrite) {
|
|
18619
|
+
let xml = await this.sendXml({
|
|
18620
|
+
cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
|
|
18621
|
+
channel: ch,
|
|
18622
|
+
...timeoutOpts
|
|
18623
|
+
});
|
|
18624
|
+
if (patch.dayNight !== void 0) {
|
|
18625
|
+
const normalized = normalizeDayNightMode(patch.dayNight);
|
|
18626
|
+
xml = patchNestedTag(xml, "DayNight", "mode", normalized);
|
|
18627
|
+
}
|
|
18628
|
+
if (patch.exposure !== void 0) {
|
|
18629
|
+
xml = patchNestedTag(
|
|
18630
|
+
xml,
|
|
18631
|
+
"Exposure",
|
|
18632
|
+
"mode",
|
|
18633
|
+
patch.exposure.toLowerCase()
|
|
18634
|
+
);
|
|
18635
|
+
}
|
|
18636
|
+
if (patch.binningMode !== void 0) {
|
|
18637
|
+
xml = applyXmlTagPatch(xml, "binning_mode", patch.binningMode);
|
|
18638
|
+
}
|
|
18639
|
+
if (patch.hdr !== void 0) {
|
|
18640
|
+
xml = applyXmlTagPatch(xml, "hdrSwitch", patch.hdr);
|
|
18641
|
+
}
|
|
18642
|
+
await this.sendXml({
|
|
18643
|
+
cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
|
|
18644
|
+
channel: ch,
|
|
18645
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18646
|
+
...timeoutOpts
|
|
18647
|
+
});
|
|
18648
|
+
}
|
|
18649
|
+
if (patch.dayNightThreshold !== void 0) {
|
|
18650
|
+
let xml = await this.sendXml({
|
|
18651
|
+
cmdId: BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
18652
|
+
channel: ch,
|
|
18653
|
+
...timeoutOpts
|
|
18654
|
+
});
|
|
18655
|
+
xml = applyXmlTagPatch(xml, "cur", patch.dayNightThreshold);
|
|
18656
|
+
await this.sendXml({
|
|
18657
|
+
cmdId: BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
|
|
18658
|
+
channel: ch,
|
|
18659
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18660
|
+
...timeoutOpts
|
|
18661
|
+
});
|
|
18662
|
+
}
|
|
18663
|
+
}
|
|
18664
|
+
/**
|
|
18665
|
+
* GetIsp via Baichuan (cmdId=26). Convenience alias of
|
|
18666
|
+
* `getVideoInput()` so callers that switched from CGI keep the
|
|
18667
|
+
* familiar name. Both return the merged VideoInput +
|
|
18668
|
+
* InputAdvanceCfg blob.
|
|
18669
|
+
*/
|
|
18670
|
+
async getIsp(channel, options) {
|
|
18671
|
+
return this.getVideoInput(channel, options);
|
|
18672
|
+
}
|
|
18673
|
+
/** GetImage via Baichuan (cmdId=26). Same payload as `getIsp` —
|
|
18674
|
+
* Reolink merged VideoInput + InputAdvanceCfg under one cmdId. */
|
|
18675
|
+
async getImage(channel, options) {
|
|
18676
|
+
return this.getVideoInput(channel, options);
|
|
18677
|
+
}
|
|
18678
|
+
/**
|
|
18679
|
+
* GetIrLights via Baichuan (cmdId=208). Returns LedState block:
|
|
18680
|
+
* `IRLedBrightness`, `state` (ir on/off), `lightState` (status LED
|
|
18681
|
+
* open/close), `doorbellLightState`. Mirrors reolink_aio's
|
|
18682
|
+
* `get_status_led`.
|
|
18683
|
+
*/
|
|
18684
|
+
async getIrLights(channel, options) {
|
|
18685
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
18686
|
+
cmdId: BC_CMD_ID_GET_LED_STATE,
|
|
18687
|
+
...channel != null ? { channel } : {},
|
|
18688
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18689
|
+
});
|
|
18690
|
+
return parseXmlFragmentToJson(xml);
|
|
18691
|
+
}
|
|
18692
|
+
/**
|
|
18693
|
+
* SetIrLights via Baichuan (cmdId=209, read via cmdId=208). Patches
|
|
18694
|
+
* IR LED + status LED + doorbell LED + IR brightness. Mirrors
|
|
18695
|
+
* reolink_aio's `set_status_led`.
|
|
18696
|
+
*
|
|
18697
|
+
* @param channel - Channel number (0-based)
|
|
18698
|
+
* @param patch - `irState` ("On" | "Off" | "Auto"), `lightState`
|
|
18699
|
+
* (status LED), `doorbellLightState`, `irBrightness` (0..255).
|
|
18700
|
+
* Camera-side accepts lowercase strings (`open`/`close`); the
|
|
18701
|
+
* helper normalizes from the friendly variants.
|
|
18702
|
+
*/
|
|
18703
|
+
async setIrLights(channel, patch, options) {
|
|
18704
|
+
const ch = this.normalizeChannel(channel);
|
|
18705
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
18706
|
+
let xml = await this.sendXml({
|
|
18707
|
+
cmdId: BC_CMD_ID_GET_LED_STATE,
|
|
18708
|
+
channel: ch,
|
|
18709
|
+
...timeoutOpts
|
|
18710
|
+
});
|
|
18711
|
+
if (patch.lightState !== void 0) {
|
|
18712
|
+
xml = applyXmlTagPatch(
|
|
18713
|
+
xml,
|
|
18714
|
+
"lightState",
|
|
18715
|
+
patch.lightState === "On" ? "open" : "close"
|
|
18716
|
+
);
|
|
18717
|
+
}
|
|
18718
|
+
if (patch.doorbellLightState !== void 0) {
|
|
18719
|
+
xml = applyXmlTagPatch(
|
|
18720
|
+
xml,
|
|
18721
|
+
"doorbellLightState",
|
|
18722
|
+
normalizeOpenClose(patch.doorbellLightState)
|
|
18723
|
+
);
|
|
18724
|
+
}
|
|
18725
|
+
if (patch.irState !== void 0) {
|
|
18726
|
+
const v = String(patch.irState);
|
|
18727
|
+
const out = v === "Off" ? "close" : v.toLowerCase();
|
|
18728
|
+
xml = applyXmlTagPatch(xml, "state", out);
|
|
18729
|
+
}
|
|
18730
|
+
if (patch.irBrightness !== void 0) {
|
|
18731
|
+
xml = applyXmlTagPatch(xml, "IRLedBrightness", patch.irBrightness);
|
|
18732
|
+
}
|
|
18733
|
+
await this.sendXml({
|
|
18734
|
+
cmdId: BC_CMD_ID_SET_LED_STATE,
|
|
18735
|
+
channel: ch,
|
|
18736
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18737
|
+
...timeoutOpts
|
|
18738
|
+
});
|
|
18739
|
+
}
|
|
18740
|
+
/**
|
|
18741
|
+
* SetAudioCfg via Baichuan (cmdId=265, read via cmdId=264). Patches
|
|
18742
|
+
* volume / talk-and-reply / visitor settings. Mirrors reolink_aio's
|
|
18743
|
+
* `SetAudioCfg`.
|
|
18744
|
+
*/
|
|
18745
|
+
async setAudioCfg(channel, patch, options) {
|
|
18746
|
+
const ch = this.normalizeChannel(channel);
|
|
18747
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
18748
|
+
let xml = await this.sendXml({
|
|
18749
|
+
cmdId: BC_CMD_ID_GET_AUDIO_CFG,
|
|
18750
|
+
channel: ch,
|
|
18751
|
+
...timeoutOpts
|
|
18752
|
+
});
|
|
18753
|
+
xml = applyXmlTagPatch(xml, "volume", patch.volume);
|
|
18754
|
+
xml = applyXmlTagPatch(
|
|
18755
|
+
xml,
|
|
18756
|
+
"talkAndReplyVolume",
|
|
18757
|
+
patch.talkAndReplyVolume
|
|
18758
|
+
);
|
|
18759
|
+
xml = applyXmlTagPatch(xml, "visitorVolume", patch.visitorVolume);
|
|
18760
|
+
xml = applyXmlTagPatch(xml, "visitorLoudspeaker", patch.visitorLoudspeaker);
|
|
18761
|
+
await this.sendXml({
|
|
18762
|
+
cmdId: BC_CMD_ID_SET_AUDIO_CFG,
|
|
18763
|
+
channel: ch,
|
|
18764
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18765
|
+
...timeoutOpts
|
|
18766
|
+
});
|
|
18767
|
+
}
|
|
18768
|
+
/**
|
|
18769
|
+
* GetMask (privacy mask) via Baichuan (cmdId=52). Returns the
|
|
18770
|
+
* `<Shelter>` block — `enable` flag + `shelterList`. Mirrors
|
|
18771
|
+
* reolink_aio's `GetMask`.
|
|
18772
|
+
*/
|
|
18773
|
+
async getMask(channel, options) {
|
|
18774
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
18775
|
+
cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
|
|
18776
|
+
...channel != null ? { channel } : {},
|
|
18777
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18778
|
+
});
|
|
18779
|
+
return parseXmlFragmentToJson(xml);
|
|
18780
|
+
}
|
|
18781
|
+
/**
|
|
18782
|
+
* SetMask (privacy mask) via Baichuan (cmdId=53, read via cmdId=52).
|
|
18783
|
+
* Toggles the `<Shelter><enable>` flag. Mirrors reolink_aio's
|
|
18784
|
+
* `SetMask` (which only touches enable too — shelter zone editing
|
|
18785
|
+
* goes through a separate flow).
|
|
18786
|
+
*/
|
|
18787
|
+
async setMask(channel, patch, options) {
|
|
18788
|
+
const ch = this.normalizeChannel(channel);
|
|
18789
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
18790
|
+
let xml = await this.sendXml({
|
|
18791
|
+
cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
|
|
18792
|
+
channel: ch,
|
|
18793
|
+
...timeoutOpts
|
|
18794
|
+
});
|
|
18795
|
+
if (patch.enable !== void 0) {
|
|
18796
|
+
xml = applyXmlTagPatch(xml, "enable", patch.enable ? 1 : 0);
|
|
18797
|
+
}
|
|
18798
|
+
await this.sendXml({
|
|
18799
|
+
cmdId: BC_CMD_ID_SET_PRIVACY_MASK,
|
|
18800
|
+
channel: ch,
|
|
18801
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18802
|
+
...timeoutOpts
|
|
18803
|
+
});
|
|
18804
|
+
}
|
|
18805
|
+
/**
|
|
18806
|
+
* GetAudioNoise via Baichuan (cmdId=439). Reads `enable` + `level`
|
|
18807
|
+
* from the aiDenoise block. Mirrors reolink_aio's `GetAudioNoise`.
|
|
18808
|
+
*
|
|
18809
|
+
* Note: `getAiDenoise` already returns the same payload typed as
|
|
18810
|
+
* `AiDenoiseConfig`. This getter exists for naming parity with
|
|
18811
|
+
* reolink_aio + the reolink CGI.
|
|
18812
|
+
*/
|
|
18813
|
+
async getAudioNoise(channel, options) {
|
|
18814
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
18815
|
+
cmdId: BC_CMD_ID_GET_AI_DENOISE,
|
|
18816
|
+
...channel != null ? { channel } : {},
|
|
18817
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18818
|
+
});
|
|
18819
|
+
return parseXmlFragmentToJson(xml);
|
|
18820
|
+
}
|
|
18821
|
+
/**
|
|
18822
|
+
* SetAudioNoise via Baichuan (cmdId=440, read via cmdId=439).
|
|
18823
|
+
* Mirrors reolink_aio's `SetAudioNoise` — `level <= 0` flips the
|
|
18824
|
+
* enable flag off; positive values turn it on and update the level.
|
|
18825
|
+
*/
|
|
18826
|
+
async setAudioNoise(channel, level, options) {
|
|
18827
|
+
const ch = this.normalizeChannel(channel);
|
|
18828
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
18829
|
+
let xml = await this.sendXml({
|
|
18830
|
+
cmdId: BC_CMD_ID_GET_AI_DENOISE,
|
|
18831
|
+
channel: ch,
|
|
18832
|
+
...timeoutOpts
|
|
18833
|
+
});
|
|
18834
|
+
xml = applyXmlTagPatch(xml, "enable", level > 0 ? 1 : 0);
|
|
18835
|
+
if (level > 0) {
|
|
18836
|
+
xml = applyXmlTagPatch(xml, "level", level);
|
|
18837
|
+
}
|
|
18838
|
+
await this.sendXml({
|
|
18839
|
+
cmdId: BC_CMD_ID_SET_AI_DENOISE,
|
|
18840
|
+
channel: ch,
|
|
18841
|
+
payloadXml: ensureXmlHeader(xml),
|
|
18842
|
+
...timeoutOpts
|
|
18843
|
+
});
|
|
18844
|
+
}
|
|
18845
|
+
/**
|
|
18846
|
+
* GetAutoFocus via Baichuan (cmdId=224). Returns the `<AutoFocus>`
|
|
18847
|
+
* block — only `disable` (0 = AF on, 1 = AF off). Mirrors
|
|
18848
|
+
* reolink_aio's `GetAutoFocus`.
|
|
18849
|
+
*/
|
|
18850
|
+
async getAutoFocus(channel, options) {
|
|
18851
|
+
const ch = this.normalizeChannel(channel);
|
|
18852
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
18853
|
+
cmdId: BC_CMD_ID_GET_AUTO_FOCUS,
|
|
18854
|
+
channel: ch,
|
|
18855
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18856
|
+
});
|
|
18857
|
+
return parseXmlFragmentToJson(xml);
|
|
18858
|
+
}
|
|
18859
|
+
/**
|
|
18860
|
+
* SetAutoFocus via Baichuan (cmdId=225). Mirrors reolink_aio's
|
|
18861
|
+
* `SetAutoFocus`. Note: write-only command — the payload is built
|
|
18862
|
+
* from scratch (no read-modify-write needed).
|
|
18863
|
+
*/
|
|
18864
|
+
async setAutoFocus(channel, disable, options) {
|
|
18865
|
+
const ch = this.normalizeChannel(channel);
|
|
18866
|
+
const disableVal = disable ? 1 : 0;
|
|
18867
|
+
const payloadXml = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
18868
|
+
<body>
|
|
18869
|
+
<AutoFocus version="1.1">
|
|
18870
|
+
<channelId>${ch}</channelId>
|
|
18871
|
+
<disable>${disableVal}</disable>
|
|
18872
|
+
</AutoFocus>
|
|
18873
|
+
</body>`;
|
|
18874
|
+
await this.sendXml({
|
|
18875
|
+
cmdId: BC_CMD_ID_SET_AUTO_FOCUS,
|
|
18876
|
+
channel: ch,
|
|
18877
|
+
payloadXml,
|
|
18878
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
18879
|
+
});
|
|
18880
|
+
}
|
|
18217
18881
|
/**
|
|
18218
18882
|
* Passthrough to ReolinkCgiApi.getAllChannelsBatteryInfo.
|
|
18219
18883
|
* Fetches battery info for all channels via CGI (merged with channel status sleep flag).
|
|
@@ -19382,8 +20046,8 @@ ${scheduleItems}
|
|
|
19382
20046
|
);
|
|
19383
20047
|
let args;
|
|
19384
20048
|
if (useMpegTsMuxer) {
|
|
19385
|
-
MpegTsMuxer
|
|
19386
|
-
tsMuxer
|
|
20049
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
20050
|
+
tsMuxer.reset();
|
|
19387
20051
|
args = [
|
|
19388
20052
|
"-hide_banner",
|
|
19389
20053
|
"-loglevel",
|
|
@@ -19547,7 +20211,7 @@ ${scheduleItems}
|
|
|
19547
20211
|
startFfmpeg(videoType);
|
|
19548
20212
|
frameCount++;
|
|
19549
20213
|
if (useMpegTsMuxer && tsMuxer) {
|
|
19550
|
-
const tsData = tsMuxer.
|
|
20214
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
19551
20215
|
input.write(tsData);
|
|
19552
20216
|
} else {
|
|
19553
20217
|
if (videoType === "H264") input.write(H264_AUD);
|
|
@@ -19836,8 +20500,8 @@ ${scheduleItems}
|
|
|
19836
20500
|
logger?.log?.(
|
|
19837
20501
|
`[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
|
|
19838
20502
|
);
|
|
19839
|
-
MpegTsMuxer
|
|
19840
|
-
tsMuxer
|
|
20503
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
20504
|
+
tsMuxer.reset();
|
|
19841
20505
|
const args = [
|
|
19842
20506
|
"-hide_banner",
|
|
19843
20507
|
"-loglevel",
|
|
@@ -20002,7 +20666,7 @@ ${scheduleItems}
|
|
|
20002
20666
|
startFfmpeg(videoType);
|
|
20003
20667
|
frameCount++;
|
|
20004
20668
|
if (tsMuxer) {
|
|
20005
|
-
const tsData = tsMuxer.
|
|
20669
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
20006
20670
|
input.write(tsData);
|
|
20007
20671
|
}
|
|
20008
20672
|
if (frameCount === 1) {
|
|
@@ -21414,9 +22078,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21414
22078
|
const msg = fmtErr(e);
|
|
21415
22079
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
21416
22080
|
};
|
|
21417
|
-
const withRetries = async (label, max, op, shouldRetry) => {
|
|
22081
|
+
const withRetries = async (label, max, op, shouldRetry, isAborted) => {
|
|
21418
22082
|
let lastErr;
|
|
21419
22083
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
22084
|
+
if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
|
|
21420
22085
|
try {
|
|
21421
22086
|
if (attempt > 1) {
|
|
21422
22087
|
logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
|
|
@@ -21425,7 +22090,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21425
22090
|
} catch (e) {
|
|
21426
22091
|
lastErr = e;
|
|
21427
22092
|
const msg = fmtErr(e);
|
|
21428
|
-
const retryable = attempt < max && shouldRetry(e);
|
|
22093
|
+
const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
|
|
21429
22094
|
logger?.log?.(
|
|
21430
22095
|
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
21431
22096
|
);
|
|
@@ -21435,6 +22100,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21435
22100
|
}
|
|
21436
22101
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
|
|
21437
22102
|
};
|
|
22103
|
+
const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
|
|
22104
|
+
let raceWon = false;
|
|
22105
|
+
const methodErrors = /* @__PURE__ */ new Map();
|
|
22106
|
+
try {
|
|
22107
|
+
return await Promise.any(
|
|
22108
|
+
methods.map(async (m) => {
|
|
22109
|
+
try {
|
|
22110
|
+
const result = await loginAndDetect(m, () => raceWon);
|
|
22111
|
+
raceWon = true;
|
|
22112
|
+
return result;
|
|
22113
|
+
} catch (e) {
|
|
22114
|
+
if (!raceWon) methodErrors.set(m, fmtErr(e));
|
|
22115
|
+
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
|
|
22116
|
+
throw e;
|
|
22117
|
+
}
|
|
22118
|
+
})
|
|
22119
|
+
);
|
|
22120
|
+
} catch (e) {
|
|
22121
|
+
if (e instanceof AggregateError) {
|
|
22122
|
+
const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
|
|
22123
|
+
throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
|
|
22124
|
+
}
|
|
22125
|
+
throw e;
|
|
22126
|
+
}
|
|
22127
|
+
};
|
|
21438
22128
|
const effectiveUid = normalizeUid(uid);
|
|
21439
22129
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
21440
22130
|
const isReachable = await pingHost(host);
|
|
@@ -21464,9 +22154,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21464
22154
|
normalizedUid = normalizedDiscovered;
|
|
21465
22155
|
}
|
|
21466
22156
|
const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
22157
|
+
return await runUdpMethodsParallel(
|
|
22158
|
+
methodsToTry,
|
|
22159
|
+
async (m, isAborted) => {
|
|
21470
22160
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
21471
22161
|
const udpApi = await withRetries(
|
|
21472
22162
|
`UDP(${m})`,
|
|
@@ -21489,11 +22179,14 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21489
22179
|
throw e;
|
|
21490
22180
|
}
|
|
21491
22181
|
},
|
|
21492
|
-
shouldRetryUdp
|
|
22182
|
+
shouldRetryUdp,
|
|
22183
|
+
isAborted
|
|
21493
22184
|
);
|
|
21494
|
-
const deviceInfo = await
|
|
21495
|
-
|
|
21496
|
-
|
|
22185
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
22186
|
+
udpApi.getInfo(),
|
|
22187
|
+
udpApi.getDeviceCapabilities(),
|
|
22188
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
22189
|
+
]);
|
|
21497
22190
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
21498
22191
|
const model = deviceInfo.type?.trim();
|
|
21499
22192
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -21532,14 +22225,8 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21532
22225
|
channelNum: 1,
|
|
21533
22226
|
api: udpApi
|
|
21534
22227
|
};
|
|
21535
|
-
}
|
|
21536
|
-
|
|
21537
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
21538
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
21539
|
-
}
|
|
21540
|
-
}
|
|
21541
|
-
throw new Error(
|
|
21542
|
-
`Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
|
|
22228
|
+
},
|
|
22229
|
+
"Forced UDP autodetect failed for all methods."
|
|
21543
22230
|
);
|
|
21544
22231
|
}
|
|
21545
22232
|
let tcpApi;
|
|
@@ -21592,54 +22279,57 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21592
22279
|
}
|
|
21593
22280
|
return void 0;
|
|
21594
22281
|
};
|
|
21595
|
-
const infoProbe = await
|
|
21596
|
-
|
|
21597
|
-
|
|
22282
|
+
const [infoProbe, supportProbe] = await Promise.all([
|
|
22283
|
+
runProbeVariants(
|
|
22284
|
+
"getInfo",
|
|
22285
|
+
[
|
|
22286
|
+
{
|
|
22287
|
+
variant: "cmd80 class=0x6414",
|
|
22288
|
+
op: () => api.getInfo(void 0, {
|
|
22289
|
+
timeoutMs: 2500,
|
|
22290
|
+
messageClass: BC_CLASS_MODERN_24
|
|
22291
|
+
})
|
|
22292
|
+
},
|
|
22293
|
+
{
|
|
22294
|
+
variant: "cmd80 class=0x6614",
|
|
22295
|
+
op: () => api.getInfo(void 0, {
|
|
22296
|
+
timeoutMs: 3e3,
|
|
22297
|
+
messageClass: BC_CLASS_MODERN_20
|
|
22298
|
+
})
|
|
22299
|
+
},
|
|
22300
|
+
{
|
|
22301
|
+
variant: "cmd318(ch0) class=0x6414",
|
|
22302
|
+
op: () => api.getInfo(0, {
|
|
22303
|
+
timeoutMs: 3e3,
|
|
22304
|
+
messageClass: BC_CLASS_MODERN_24
|
|
22305
|
+
})
|
|
22306
|
+
},
|
|
22307
|
+
{
|
|
22308
|
+
variant: "cmd318(ch0) class=0x6614",
|
|
22309
|
+
op: () => api.getInfo(0, {
|
|
22310
|
+
timeoutMs: 3500,
|
|
22311
|
+
messageClass: BC_CLASS_MODERN_20
|
|
22312
|
+
})
|
|
22313
|
+
}
|
|
22314
|
+
]
|
|
22315
|
+
),
|
|
22316
|
+
// Support probes (cmd 199). Some firmwares may not support it or are slow.
|
|
22317
|
+
runProbeVariants("getSupportInfo", [
|
|
21598
22318
|
{
|
|
21599
|
-
variant: "
|
|
21600
|
-
op: () => api.
|
|
22319
|
+
variant: "cmd199 class=0x6414",
|
|
22320
|
+
op: () => api.getSupportInfo({
|
|
21601
22321
|
timeoutMs: 2500,
|
|
21602
22322
|
messageClass: BC_CLASS_MODERN_24
|
|
21603
22323
|
})
|
|
21604
22324
|
},
|
|
21605
22325
|
{
|
|
21606
|
-
variant: "
|
|
21607
|
-
op: () => api.
|
|
21608
|
-
timeoutMs: 3e3,
|
|
21609
|
-
messageClass: BC_CLASS_MODERN_20
|
|
21610
|
-
})
|
|
21611
|
-
},
|
|
21612
|
-
{
|
|
21613
|
-
variant: "cmd318(ch0) class=0x6414",
|
|
21614
|
-
op: () => api.getInfo(0, {
|
|
21615
|
-
timeoutMs: 3e3,
|
|
21616
|
-
messageClass: BC_CLASS_MODERN_24
|
|
21617
|
-
})
|
|
21618
|
-
},
|
|
21619
|
-
{
|
|
21620
|
-
variant: "cmd318(ch0) class=0x6614",
|
|
21621
|
-
op: () => api.getInfo(0, {
|
|
22326
|
+
variant: "cmd199 class=0x6614",
|
|
22327
|
+
op: () => api.getSupportInfo({
|
|
21622
22328
|
timeoutMs: 3500,
|
|
21623
22329
|
messageClass: BC_CLASS_MODERN_20
|
|
21624
22330
|
})
|
|
21625
22331
|
}
|
|
21626
|
-
]
|
|
21627
|
-
);
|
|
21628
|
-
const supportProbe = await runProbeVariants("getSupportInfo", [
|
|
21629
|
-
{
|
|
21630
|
-
variant: "cmd199 class=0x6414",
|
|
21631
|
-
op: () => api.getSupportInfo({
|
|
21632
|
-
timeoutMs: 2500,
|
|
21633
|
-
messageClass: BC_CLASS_MODERN_24
|
|
21634
|
-
})
|
|
21635
|
-
},
|
|
21636
|
-
{
|
|
21637
|
-
variant: "cmd199 class=0x6614",
|
|
21638
|
-
op: () => api.getSupportInfo({
|
|
21639
|
-
timeoutMs: 3500,
|
|
21640
|
-
messageClass: BC_CLASS_MODERN_20
|
|
21641
|
-
})
|
|
21642
|
-
}
|
|
22332
|
+
])
|
|
21643
22333
|
]);
|
|
21644
22334
|
const deviceInfo = infoProbe?.value;
|
|
21645
22335
|
const support = supportProbe?.value;
|
|
@@ -21731,9 +22421,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21731
22421
|
}
|
|
21732
22422
|
try {
|
|
21733
22423
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
21734
|
-
const deviceInfo = await
|
|
21735
|
-
|
|
21736
|
-
|
|
22424
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
22425
|
+
udpApi.getInfo(),
|
|
22426
|
+
udpApi.getDeviceCapabilities(),
|
|
22427
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
22428
|
+
]);
|
|
21737
22429
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
21738
22430
|
const model = deviceInfo.type?.trim();
|
|
21739
22431
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -21777,21 +22469,17 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21777
22469
|
};
|
|
21778
22470
|
};
|
|
21779
22471
|
const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
21780
|
-
const
|
|
21781
|
-
|
|
21782
|
-
|
|
22472
|
+
const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
|
|
22473
|
+
return await runUdpMethodsParallel(
|
|
22474
|
+
viableMethods,
|
|
22475
|
+
async (m, isAborted) => {
|
|
21783
22476
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
21784
22477
|
const udpApi = await withRetries(
|
|
21785
22478
|
`UDP(${m})`,
|
|
21786
22479
|
maxRetries,
|
|
21787
22480
|
async (attempt) => {
|
|
21788
|
-
const apiInputs = {
|
|
21789
|
-
|
|
21790
|
-
udpDiscoveryMethod: m
|
|
21791
|
-
};
|
|
21792
|
-
if (normalizedUid) {
|
|
21793
|
-
apiInputs.uid = normalizedUid;
|
|
21794
|
-
}
|
|
22481
|
+
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
22482
|
+
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
21795
22483
|
const api = createBaichuanApi(apiInputs, "udp");
|
|
21796
22484
|
try {
|
|
21797
22485
|
await api.login();
|
|
@@ -21806,20 +22494,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21806
22494
|
throw e;
|
|
21807
22495
|
}
|
|
21808
22496
|
},
|
|
21809
|
-
shouldRetryUdp
|
|
22497
|
+
shouldRetryUdp,
|
|
22498
|
+
isAborted
|
|
21810
22499
|
);
|
|
21811
|
-
return
|
|
21812
|
-
}
|
|
21813
|
-
|
|
21814
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
21815
|
-
try {
|
|
21816
|
-
} catch {
|
|
21817
|
-
}
|
|
21818
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
21819
|
-
}
|
|
21820
|
-
}
|
|
21821
|
-
throw new Error(
|
|
21822
|
-
`UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
|
|
22500
|
+
return detectOverUdpApi(udpApi, m);
|
|
22501
|
+
},
|
|
22502
|
+
"UDP discovery failed for all methods."
|
|
21823
22503
|
);
|
|
21824
22504
|
} catch (udpError) {
|
|
21825
22505
|
logger?.log?.(
|
|
@@ -21851,12 +22531,14 @@ export {
|
|
|
21851
22531
|
BaichuanEventEmitter,
|
|
21852
22532
|
createNativeStream,
|
|
21853
22533
|
BaichuanRtspServer,
|
|
22534
|
+
MpegTsMuxer,
|
|
21854
22535
|
flattenAbilitiesForChannel,
|
|
21855
22536
|
abilitiesHasAny,
|
|
21856
22537
|
parseSupportXml,
|
|
21857
22538
|
getSupportItemForChannel,
|
|
21858
22539
|
computeDeviceCapabilities,
|
|
21859
22540
|
xmlIndicatesFloodlight,
|
|
22541
|
+
decideSleepInferenceTransition,
|
|
21860
22542
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
21861
22543
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
21862
22544
|
DUAL_LENS_MODELS,
|
|
@@ -21878,4 +22560,4 @@ export {
|
|
|
21878
22560
|
isTcpFailureThatShouldFallbackToUdp,
|
|
21879
22561
|
autoDetectDeviceType
|
|
21880
22562
|
};
|
|
21881
|
-
//# sourceMappingURL=chunk-
|
|
22563
|
+
//# sourceMappingURL=chunk-HGQ53FB3.js.map
|