@apocaliss92/nodelink-js 0.4.7 → 0.4.8
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-HJDH4GPP.js} +2 -2
- package/dist/{chunk-GKLOJJ34.js → chunk-VBYF3BQX.js} +639 -341
- package/dist/chunk-VBYF3BQX.js.map +1 -0
- package/dist/{chunk-TR3V5FTO.js → chunk-YKKQDUKU.js} +3 -3
- package/dist/{chunk-TR3V5FTO.js.map → chunk-YKKQDUKU.js.map} +1 -1
- package/dist/cli/rtsp-server.cjs +607 -311
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +793 -328
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +254 -6
- package/dist/index.d.ts +238 -5
- package/dist/index.js +188 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GKLOJJ34.js.map +0 -1
- /package/dist/{DiagnosticsTools-UMN4C7SY.js.map → DiagnosticsTools-HJDH4GPP.js.map} +0 -0
|
@@ -144,7 +144,7 @@ import {
|
|
|
144
144
|
talkTraceLog,
|
|
145
145
|
traceLog,
|
|
146
146
|
xmlEscape
|
|
147
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-YKKQDUKU.js";
|
|
148
148
|
|
|
149
149
|
// src/protocol/framing.ts
|
|
150
150
|
function encodeHeader(h) {
|
|
@@ -5325,19 +5325,34 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
5325
5325
|
}
|
|
5326
5326
|
});
|
|
5327
5327
|
streamStarted = true;
|
|
5328
|
-
|
|
5328
|
+
const signal = options?.signal;
|
|
5329
|
+
while (!closed && !signal?.aborted) {
|
|
5329
5330
|
if (frameQueue.length > 0) {
|
|
5330
5331
|
const frame = frameQueue.shift();
|
|
5331
5332
|
yield frame;
|
|
5332
5333
|
} else {
|
|
5333
5334
|
await new Promise((resolve) => {
|
|
5334
5335
|
frameResolve = resolve;
|
|
5335
|
-
setTimeout(() => {
|
|
5336
|
+
const timer = setTimeout(() => {
|
|
5336
5337
|
if (frameResolve === resolve) {
|
|
5337
5338
|
frameResolve = null;
|
|
5338
5339
|
resolve();
|
|
5339
5340
|
}
|
|
5340
5341
|
}, 1e3);
|
|
5342
|
+
if (signal) {
|
|
5343
|
+
const onAbort = () => {
|
|
5344
|
+
clearTimeout(timer);
|
|
5345
|
+
if (frameResolve === resolve) frameResolve = null;
|
|
5346
|
+
resolve();
|
|
5347
|
+
};
|
|
5348
|
+
if (signal.aborted) {
|
|
5349
|
+
clearTimeout(timer);
|
|
5350
|
+
frameResolve = null;
|
|
5351
|
+
resolve();
|
|
5352
|
+
} else {
|
|
5353
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5341
5356
|
});
|
|
5342
5357
|
}
|
|
5343
5358
|
}
|
|
@@ -5513,13 +5528,14 @@ var NativeStreamFanout = class {
|
|
|
5513
5528
|
source = null;
|
|
5514
5529
|
running = false;
|
|
5515
5530
|
pumpPromise = null;
|
|
5531
|
+
abort = new AbortController();
|
|
5516
5532
|
constructor(opts) {
|
|
5517
5533
|
this.opts = opts;
|
|
5518
5534
|
}
|
|
5519
5535
|
start() {
|
|
5520
5536
|
if (this.running) return;
|
|
5521
5537
|
this.running = true;
|
|
5522
|
-
this.source = this.opts.createSource();
|
|
5538
|
+
this.source = this.opts.createSource(this.abort.signal);
|
|
5523
5539
|
this.pumpPromise = (async () => {
|
|
5524
5540
|
try {
|
|
5525
5541
|
for await (const frame of this.source) {
|
|
@@ -5565,6 +5581,7 @@ var NativeStreamFanout = class {
|
|
|
5565
5581
|
this.source = null;
|
|
5566
5582
|
for (const q of this.queues.values()) q.close();
|
|
5567
5583
|
this.queues.clear();
|
|
5584
|
+
this.abort.abort();
|
|
5568
5585
|
try {
|
|
5569
5586
|
await src?.return(void 0);
|
|
5570
5587
|
} catch {
|
|
@@ -5603,9 +5620,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5603
5620
|
requireAuth;
|
|
5604
5621
|
authNonces = /* @__PURE__ */ new Map();
|
|
5605
5622
|
// Track nonces per client
|
|
5606
|
-
AUTH_REALM
|
|
5623
|
+
AUTH_REALM;
|
|
5607
5624
|
NONCE_TIMEOUT_MS = 3e5;
|
|
5608
5625
|
// 5 minutes
|
|
5626
|
+
lazyMetadata;
|
|
5609
5627
|
// Client tracking
|
|
5610
5628
|
connectedClients = /* @__PURE__ */ new Set();
|
|
5611
5629
|
// Set of client IDs (IP:port)
|
|
@@ -5617,8 +5635,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5617
5635
|
// Track all client resources for cleanup
|
|
5618
5636
|
clientResources = /* @__PURE__ */ new Map();
|
|
5619
5637
|
isRtspDebugEnabled() {
|
|
5620
|
-
|
|
5621
|
-
|
|
5638
|
+
try {
|
|
5639
|
+
if (this.api.isClosed) {
|
|
5640
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
5641
|
+
}
|
|
5642
|
+
const dbg = this.api.client.getDebugConfig();
|
|
5643
|
+
return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
5644
|
+
} catch {
|
|
5645
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
5646
|
+
}
|
|
5622
5647
|
}
|
|
5623
5648
|
rtspDebugLog(message) {
|
|
5624
5649
|
if (!this.isRtspDebugEnabled()) return;
|
|
@@ -5640,10 +5665,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5640
5665
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
5641
5666
|
nativeFanout = null;
|
|
5642
5667
|
noClientAutoStopTimer;
|
|
5668
|
+
/** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
|
|
5669
|
+
noFrameDeadlineTimer;
|
|
5643
5670
|
/** After last RTSP client; 0 = never auto-stop native stream. */
|
|
5644
5671
|
nativeStreamIdleStopMs;
|
|
5645
5672
|
/** Primed-but-no-PLAY timeout; 0 = disabled. */
|
|
5646
5673
|
nativeStreamPrimeIdleStopMs;
|
|
5674
|
+
/**
|
|
5675
|
+
* Max time to wait for the first camera frame after stream start.
|
|
5676
|
+
* If no frames arrive within this window, the native stream is stopped
|
|
5677
|
+
* (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
|
|
5678
|
+
* firing and waking the camera when no real viewer is watching.
|
|
5679
|
+
* 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
|
|
5680
|
+
*/
|
|
5681
|
+
nativeStreamNoFrameDeadlineMs;
|
|
5647
5682
|
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
5648
5683
|
// When a new client connects while the stream is already running it does not need
|
|
5649
5684
|
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
@@ -5769,14 +5804,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5769
5804
|
this.logger = options.logger ?? console;
|
|
5770
5805
|
this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
|
|
5771
5806
|
this.deviceId = options.deviceId;
|
|
5772
|
-
this.externalListener = options.externalListener ?? false;
|
|
5807
|
+
this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
|
|
5773
5808
|
this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
|
|
5774
5809
|
this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
|
|
5775
|
-
this.
|
|
5810
|
+
this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
|
|
5811
|
+
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
5812
|
+
username: c.username,
|
|
5813
|
+
...c.password !== void 0 ? { password: c.password } : {},
|
|
5814
|
+
...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
|
|
5815
|
+
}));
|
|
5776
5816
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
5817
|
+
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
5818
|
+
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
5777
5819
|
const transport = this.api.client.getTransport();
|
|
5778
5820
|
this.flow = createRtspFlow(transport, "H264");
|
|
5779
5821
|
}
|
|
5822
|
+
/** Number of currently connected RTSP clients. */
|
|
5823
|
+
get clientCount() {
|
|
5824
|
+
return this.connectedClients.size;
|
|
5825
|
+
}
|
|
5780
5826
|
// --- Authentication helpers ---
|
|
5781
5827
|
/**
|
|
5782
5828
|
* Generate a new nonce for Digest authentication
|
|
@@ -5837,9 +5883,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5837
5883
|
this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
|
|
5838
5884
|
return false;
|
|
5839
5885
|
}
|
|
5886
|
+
if (realm !== this.AUTH_REALM) {
|
|
5887
|
+
this.rtspDebugLog(
|
|
5888
|
+
`Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
|
|
5889
|
+
);
|
|
5890
|
+
return false;
|
|
5891
|
+
}
|
|
5840
5892
|
for (const cred of this.authCredentials) {
|
|
5841
5893
|
if (username !== cred.username) continue;
|
|
5842
|
-
const ha1 = this.md5(`${cred.username}:${
|
|
5894
|
+
const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
|
|
5895
|
+
if (!ha1) continue;
|
|
5843
5896
|
const ha2 = this.md5(`${method}:${authUri || uri}`);
|
|
5844
5897
|
const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
|
|
5845
5898
|
if (response === expectedResponse) {
|
|
@@ -5868,6 +5921,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5868
5921
|
this.noClientAutoStopTimer = void 0;
|
|
5869
5922
|
}
|
|
5870
5923
|
}
|
|
5924
|
+
clearNoFrameDeadlineTimer() {
|
|
5925
|
+
if (this.noFrameDeadlineTimer) {
|
|
5926
|
+
clearTimeout(this.noFrameDeadlineTimer);
|
|
5927
|
+
this.noFrameDeadlineTimer = void 0;
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5871
5930
|
setFlowVideoType(videoType, reason) {
|
|
5872
5931
|
if (this.flow.videoType === videoType) return;
|
|
5873
5932
|
const transport = this.api.client.getTransport();
|
|
@@ -5882,25 +5941,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5882
5941
|
if (this.active) {
|
|
5883
5942
|
throw new Error("RTSP server is already active");
|
|
5884
5943
|
}
|
|
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}`
|
|
5944
|
+
if (this.lazyMetadata) {
|
|
5945
|
+
this.logger.info(
|
|
5946
|
+
`[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
|
|
5901
5947
|
);
|
|
5902
|
-
|
|
5903
|
-
|
|
5948
|
+
} else {
|
|
5949
|
+
try {
|
|
5950
|
+
const metadata = await this.api.getStreamMetadata(this.channel);
|
|
5951
|
+
const stream = metadata.streams.find((s) => s.profile === this.profile);
|
|
5952
|
+
if (stream) {
|
|
5953
|
+
this.streamMetadata = {
|
|
5954
|
+
frameRate: stream.frameRate || 25,
|
|
5955
|
+
width: stream.width,
|
|
5956
|
+
height: stream.height
|
|
5957
|
+
};
|
|
5958
|
+
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
5959
|
+
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
5960
|
+
this.setFlowVideoType(metaVideoType, "metadata");
|
|
5961
|
+
}
|
|
5962
|
+
} catch (error) {
|
|
5963
|
+
this.logger.warn(
|
|
5964
|
+
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
5965
|
+
);
|
|
5966
|
+
this.streamMetadata = { frameRate: 25 };
|
|
5967
|
+
this.setFlowVideoType("H264", "metadata unavailable");
|
|
5968
|
+
}
|
|
5904
5969
|
}
|
|
5905
5970
|
if (!this.externalListener) {
|
|
5906
5971
|
this.clientConnectionServer = net2.createServer((socket) => {
|
|
@@ -5941,6 +6006,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
5941
6006
|
}
|
|
5942
6007
|
this.handleRtspConnection(socket, initialBuffer);
|
|
5943
6008
|
}
|
|
6009
|
+
/**
|
|
6010
|
+
* Inject an already-accepted client socket from a multiplexer
|
|
6011
|
+
* (e.g. `LocalRtspMux`) that owns the listening port.
|
|
6012
|
+
*
|
|
6013
|
+
* The mux reads the first RTSP request line to determine the target path,
|
|
6014
|
+
* then hands the socket over. Any bytes already consumed during routing
|
|
6015
|
+
* are replayed back onto the socket via `unshift()` so the RTSP parser in
|
|
6016
|
+
* `handleRtspConnection` sees the complete original request.
|
|
6017
|
+
*
|
|
6018
|
+
* @param socket - Client TCP socket, already accepted by the mux.
|
|
6019
|
+
* @param preReadData - Bytes the mux has already pulled off the socket
|
|
6020
|
+
* while parsing the request line. Replayed via `socket.unshift()`
|
|
6021
|
+
* before any further reads.
|
|
6022
|
+
*/
|
|
6023
|
+
injectSocket(socket, preReadData) {
|
|
6024
|
+
if (!this.active) {
|
|
6025
|
+
socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
|
|
6026
|
+
return;
|
|
6027
|
+
}
|
|
6028
|
+
if (preReadData && preReadData.length > 0) {
|
|
6029
|
+
socket.unshift(preReadData);
|
|
6030
|
+
}
|
|
6031
|
+
this.handleRtspConnection(socket);
|
|
6032
|
+
}
|
|
5944
6033
|
/**
|
|
5945
6034
|
* Handle RTSP connection from a client.
|
|
5946
6035
|
*/
|
|
@@ -6105,6 +6194,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6105
6194
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
6106
6195
|
});
|
|
6107
6196
|
} else if (method === "DESCRIBE") {
|
|
6197
|
+
if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
|
|
6198
|
+
void this.api.ensureConnected().catch(() => {
|
|
6199
|
+
});
|
|
6200
|
+
}
|
|
6108
6201
|
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
6109
6202
|
try {
|
|
6110
6203
|
if (!this.nativeStreamActive) {
|
|
@@ -6142,6 +6235,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6142
6235
|
}
|
|
6143
6236
|
}
|
|
6144
6237
|
}
|
|
6238
|
+
if (!this.hasAudio && this.firstAudioPromise) {
|
|
6239
|
+
const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
|
|
6240
|
+
const audioPrimingStart = Date.now();
|
|
6241
|
+
try {
|
|
6242
|
+
await Promise.race([
|
|
6243
|
+
this.firstAudioPromise,
|
|
6244
|
+
new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
|
|
6245
|
+
]);
|
|
6246
|
+
} catch {
|
|
6247
|
+
}
|
|
6248
|
+
const audioPrimingElapsed = Date.now() - audioPrimingStart;
|
|
6249
|
+
if (this.hasAudio) {
|
|
6250
|
+
this.logger.info(
|
|
6251
|
+
`[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
|
|
6252
|
+
);
|
|
6253
|
+
} else {
|
|
6254
|
+
this.logger.info(
|
|
6255
|
+
`[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
|
|
6256
|
+
);
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6145
6259
|
{
|
|
6146
6260
|
const { fmtp, hasParamSets } = this.flow.getFmtp();
|
|
6147
6261
|
const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
|
|
@@ -6150,12 +6264,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6150
6264
|
);
|
|
6151
6265
|
}
|
|
6152
6266
|
const sdp = this.generateSdp();
|
|
6267
|
+
const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
|
|
6153
6268
|
sendResponse(
|
|
6154
6269
|
200,
|
|
6155
6270
|
"OK",
|
|
6156
6271
|
{
|
|
6157
6272
|
"Content-Type": "application/sdp",
|
|
6158
|
-
"Content-Base": `rtsp://${
|
|
6273
|
+
"Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
|
|
6159
6274
|
},
|
|
6160
6275
|
sdp
|
|
6161
6276
|
);
|
|
@@ -6178,7 +6293,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6178
6293
|
this.emit("client", clientId);
|
|
6179
6294
|
this.clearNoClientAutoStopTimer();
|
|
6180
6295
|
if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
|
|
6181
|
-
|
|
6296
|
+
void this.startNativeStream();
|
|
6182
6297
|
}
|
|
6183
6298
|
const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
|
|
6184
6299
|
const transport = (transportMatch?.[1] ?? "").trim();
|
|
@@ -6312,12 +6427,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
6312
6427
|
}
|
|
6313
6428
|
}
|
|
6314
6429
|
};
|
|
6430
|
+
const runProcessBuffer = () => {
|
|
6431
|
+
processBuffer().catch((err) => {
|
|
6432
|
+
this.logger.debug(
|
|
6433
|
+
`[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
|
|
6434
|
+
);
|
|
6435
|
+
try {
|
|
6436
|
+
socket.destroy();
|
|
6437
|
+
} catch {
|
|
6438
|
+
}
|
|
6439
|
+
});
|
|
6440
|
+
};
|
|
6315
6441
|
socket.on("data", (data) => {
|
|
6316
6442
|
buffer = Buffer.concat([buffer, data]);
|
|
6317
|
-
|
|
6443
|
+
runProcessBuffer();
|
|
6318
6444
|
});
|
|
6319
6445
|
if (buffer.includes("\r\n\r\n")) {
|
|
6320
|
-
|
|
6446
|
+
runProcessBuffer();
|
|
6321
6447
|
}
|
|
6322
6448
|
}
|
|
6323
6449
|
/**
|
|
@@ -7185,6 +7311,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7185
7311
|
if (this.nativeStreamActive) {
|
|
7186
7312
|
return;
|
|
7187
7313
|
}
|
|
7314
|
+
if (!this.api.isReady) {
|
|
7315
|
+
if (this.api.isClosed) {
|
|
7316
|
+
this.logger.warn?.(
|
|
7317
|
+
`[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
|
|
7318
|
+
);
|
|
7319
|
+
return;
|
|
7320
|
+
}
|
|
7321
|
+
try {
|
|
7322
|
+
this.logger.info?.(
|
|
7323
|
+
`[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
|
|
7324
|
+
);
|
|
7325
|
+
await this.api.ensureConnected();
|
|
7326
|
+
} catch (e) {
|
|
7327
|
+
this.logger.warn?.(
|
|
7328
|
+
`[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
|
|
7329
|
+
);
|
|
7330
|
+
return;
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7188
7333
|
this.nativeStreamActive = true;
|
|
7189
7334
|
this.firstFrameReceived = false;
|
|
7190
7335
|
this.firstAudioDetected = false;
|
|
@@ -7219,13 +7364,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7219
7364
|
await this.flow.startKeepAlive(this.api);
|
|
7220
7365
|
this.nativeFanout = new NativeStreamFanout({
|
|
7221
7366
|
maxQueueItems: 200,
|
|
7222
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
7367
|
+
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
7223
7368
|
variant: this.variant,
|
|
7224
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
7369
|
+
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
7370
|
+
signal
|
|
7225
7371
|
}),
|
|
7226
7372
|
onFrame: (frame) => {
|
|
7227
7373
|
if (frame.audio) {
|
|
7228
|
-
if (!this.hasAudio &&
|
|
7374
|
+
if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
|
|
7229
7375
|
const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
|
|
7230
7376
|
if (info) {
|
|
7231
7377
|
this.hasAudio = true;
|
|
@@ -7274,6 +7420,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7274
7420
|
onEnd: () => {
|
|
7275
7421
|
if (!this.nativeStreamActive) return;
|
|
7276
7422
|
this.nativeStreamActive = false;
|
|
7423
|
+
this.clearNoFrameDeadlineTimer();
|
|
7424
|
+
const hadFrames = this.firstFrameReceived;
|
|
7277
7425
|
this.firstFrameReceived = false;
|
|
7278
7426
|
this.firstFramePromise = null;
|
|
7279
7427
|
this.firstFrameResolve = null;
|
|
@@ -7298,7 +7446,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7298
7446
|
} catch {
|
|
7299
7447
|
}
|
|
7300
7448
|
}
|
|
7301
|
-
if (this.connectedClients.size > 0) {
|
|
7449
|
+
if (this.connectedClients.size > 0 && hadFrames) {
|
|
7302
7450
|
this.logger.info(
|
|
7303
7451
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
7304
7452
|
);
|
|
@@ -7310,6 +7458,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7310
7458
|
}
|
|
7311
7459
|
});
|
|
7312
7460
|
this.nativeFanout.start();
|
|
7461
|
+
this.clearNoFrameDeadlineTimer();
|
|
7462
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
7463
|
+
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
7464
|
+
this.noFrameDeadlineTimer = void 0;
|
|
7465
|
+
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
7466
|
+
this.logger.info(
|
|
7467
|
+
`[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
|
|
7468
|
+
);
|
|
7469
|
+
void this.stopNativeStream();
|
|
7470
|
+
}
|
|
7471
|
+
}, this.nativeStreamNoFrameDeadlineMs);
|
|
7472
|
+
this.noFrameDeadlineTimer?.unref?.();
|
|
7473
|
+
}
|
|
7313
7474
|
this.clearNoClientAutoStopTimer();
|
|
7314
7475
|
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
7315
7476
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
@@ -7326,6 +7487,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7326
7487
|
markFirstFrameReceived() {
|
|
7327
7488
|
if (!this.firstFrameReceived && this.firstFrameResolve) {
|
|
7328
7489
|
this.firstFrameReceived = true;
|
|
7490
|
+
this.clearNoFrameDeadlineTimer();
|
|
7329
7491
|
this.rtspDebugLog(
|
|
7330
7492
|
`First frame received from camera for profile ${this.profile}`
|
|
7331
7493
|
);
|
|
@@ -7352,6 +7514,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7352
7514
|
);
|
|
7353
7515
|
this.flow.stopKeepAlive();
|
|
7354
7516
|
this.clearNoClientAutoStopTimer();
|
|
7517
|
+
this.clearNoFrameDeadlineTimer();
|
|
7355
7518
|
this.nativeStreamActive = false;
|
|
7356
7519
|
this.firstFrameReceived = false;
|
|
7357
7520
|
this.firstFramePromise = null;
|
|
@@ -7561,6 +7724,249 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
|
|
|
7561
7724
|
}
|
|
7562
7725
|
};
|
|
7563
7726
|
|
|
7727
|
+
// src/baichuan/stream/MpegTsMuxer.ts
|
|
7728
|
+
var TS_PACKET_SIZE = 188;
|
|
7729
|
+
var TS_SYNC_BYTE = 71;
|
|
7730
|
+
var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
|
|
7731
|
+
var PID_PAT = 0;
|
|
7732
|
+
var PID_PMT = 4096;
|
|
7733
|
+
var PID_VIDEO = 256;
|
|
7734
|
+
var PID_AUDIO = 257;
|
|
7735
|
+
var STREAM_TYPE_H264 = 27;
|
|
7736
|
+
var STREAM_TYPE_H265 = 36;
|
|
7737
|
+
var STREAM_TYPE_AAC = 15;
|
|
7738
|
+
var PES_STREAM_ID_VIDEO = 224;
|
|
7739
|
+
var PES_STREAM_ID_AUDIO = 192;
|
|
7740
|
+
var PAT_PMT_INTERVAL = 40;
|
|
7741
|
+
function crc32Mpeg(data) {
|
|
7742
|
+
let crc = 4294967295;
|
|
7743
|
+
for (let i = 0; i < data.length; i++) {
|
|
7744
|
+
crc ^= data[i] << 24;
|
|
7745
|
+
for (let j = 0; j < 8; j++) {
|
|
7746
|
+
if (crc & 2147483648) {
|
|
7747
|
+
crc = (crc << 1 ^ 79764919) >>> 0;
|
|
7748
|
+
} else {
|
|
7749
|
+
crc = crc << 1 >>> 0;
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
return crc >>> 0;
|
|
7754
|
+
}
|
|
7755
|
+
function usToPts(us) {
|
|
7756
|
+
return Math.floor(us * 90 / 1e3) & 8589934591;
|
|
7757
|
+
}
|
|
7758
|
+
function encodePts(buf, offset, pts, prefix) {
|
|
7759
|
+
buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
|
|
7760
|
+
buf[offset + 1] = pts >>> 22 & 255;
|
|
7761
|
+
buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
|
|
7762
|
+
buf[offset + 3] = pts >>> 7 & 255;
|
|
7763
|
+
buf[offset + 4] = (pts & 127) << 1 | 1;
|
|
7764
|
+
}
|
|
7765
|
+
function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
|
|
7766
|
+
buf[0] = TS_SYNC_BYTE;
|
|
7767
|
+
buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
|
|
7768
|
+
buf[2] = pid & 255;
|
|
7769
|
+
buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
|
|
7770
|
+
}
|
|
7771
|
+
function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
|
|
7772
|
+
const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
|
|
7773
|
+
const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
|
|
7774
|
+
let pesOffset = 0;
|
|
7775
|
+
let outOffset = 0;
|
|
7776
|
+
let isFirst = true;
|
|
7777
|
+
while (pesOffset < pesData.length) {
|
|
7778
|
+
const remaining = pesData.length - pesOffset;
|
|
7779
|
+
const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
|
|
7780
|
+
outOffset += TS_PACKET_SIZE;
|
|
7781
|
+
if (remaining >= TS_PAYLOAD_SIZE) {
|
|
7782
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
|
|
7783
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
7784
|
+
pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
|
|
7785
|
+
pesOffset += TS_PAYLOAD_SIZE;
|
|
7786
|
+
} else {
|
|
7787
|
+
const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
|
|
7788
|
+
if (paddingNeeded === 1) {
|
|
7789
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
7790
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
7791
|
+
packet[4] = 0;
|
|
7792
|
+
pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
|
|
7793
|
+
} else {
|
|
7794
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
7795
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
7796
|
+
const adaptLen = paddingNeeded - 1;
|
|
7797
|
+
packet[4] = adaptLen;
|
|
7798
|
+
packet[5] = isFirst && isKeyframe ? 64 : 0;
|
|
7799
|
+
packet.fill(255, 6, 4 + paddingNeeded);
|
|
7800
|
+
pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
|
|
7801
|
+
}
|
|
7802
|
+
pesOffset += remaining;
|
|
7803
|
+
}
|
|
7804
|
+
isFirst = false;
|
|
7805
|
+
}
|
|
7806
|
+
return out;
|
|
7807
|
+
}
|
|
7808
|
+
function buildPat(cc) {
|
|
7809
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7810
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
7811
|
+
pkt[1] = 64 | PID_PAT >> 8 & 31;
|
|
7812
|
+
pkt[2] = PID_PAT & 255;
|
|
7813
|
+
pkt[3] = 16 | cc & 15;
|
|
7814
|
+
pkt[4] = 0;
|
|
7815
|
+
const sectionStart = 5;
|
|
7816
|
+
let i = sectionStart;
|
|
7817
|
+
pkt[i++] = 0;
|
|
7818
|
+
pkt[i++] = 176;
|
|
7819
|
+
pkt[i++] = 13;
|
|
7820
|
+
pkt[i++] = 0;
|
|
7821
|
+
pkt[i++] = 1;
|
|
7822
|
+
pkt[i++] = 193;
|
|
7823
|
+
pkt[i++] = 0;
|
|
7824
|
+
pkt[i++] = 0;
|
|
7825
|
+
pkt[i++] = 0;
|
|
7826
|
+
pkt[i++] = 1;
|
|
7827
|
+
pkt[i++] = 224 | PID_PMT >> 8 & 31;
|
|
7828
|
+
pkt[i++] = PID_PMT & 255;
|
|
7829
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
7830
|
+
pkt.writeUInt32BE(crc, i);
|
|
7831
|
+
return pkt;
|
|
7832
|
+
}
|
|
7833
|
+
function buildPmt(videoStreamType, includeAudio, cc) {
|
|
7834
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
7835
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
7836
|
+
pkt[1] = 64 | PID_PMT >> 8 & 31;
|
|
7837
|
+
pkt[2] = PID_PMT & 255;
|
|
7838
|
+
pkt[3] = 16 | cc & 15;
|
|
7839
|
+
pkt[4] = 0;
|
|
7840
|
+
const sectionStart = 5;
|
|
7841
|
+
let i = sectionStart;
|
|
7842
|
+
pkt[i++] = 2;
|
|
7843
|
+
pkt[i++] = 176;
|
|
7844
|
+
const sectionLenPos = i;
|
|
7845
|
+
i += 1;
|
|
7846
|
+
pkt[i++] = 0;
|
|
7847
|
+
pkt[i++] = 1;
|
|
7848
|
+
pkt[i++] = 193;
|
|
7849
|
+
pkt[i++] = 0;
|
|
7850
|
+
pkt[i++] = 0;
|
|
7851
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
7852
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
7853
|
+
pkt[i++] = 240;
|
|
7854
|
+
pkt[i++] = 0;
|
|
7855
|
+
pkt[i++] = videoStreamType;
|
|
7856
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
7857
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
7858
|
+
pkt[i++] = 240;
|
|
7859
|
+
pkt[i++] = 0;
|
|
7860
|
+
if (includeAudio) {
|
|
7861
|
+
pkt[i++] = STREAM_TYPE_AAC;
|
|
7862
|
+
pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
|
|
7863
|
+
pkt[i++] = PID_AUDIO & 255;
|
|
7864
|
+
pkt[i++] = 240;
|
|
7865
|
+
pkt[i++] = 0;
|
|
7866
|
+
}
|
|
7867
|
+
const sectionLen = i - sectionStart - 3 + 4;
|
|
7868
|
+
pkt[sectionLenPos] = sectionLen;
|
|
7869
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
7870
|
+
pkt.writeUInt32BE(crc, i);
|
|
7871
|
+
return pkt;
|
|
7872
|
+
}
|
|
7873
|
+
function buildVideoPes(annexBData, ptsUs, isKeyframe) {
|
|
7874
|
+
const pts = usToPts(ptsUs);
|
|
7875
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
7876
|
+
pesHeader[0] = 0;
|
|
7877
|
+
pesHeader[1] = 0;
|
|
7878
|
+
pesHeader[2] = 1;
|
|
7879
|
+
pesHeader[3] = PES_STREAM_ID_VIDEO;
|
|
7880
|
+
pesHeader[4] = 0;
|
|
7881
|
+
pesHeader[5] = 0;
|
|
7882
|
+
pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
|
|
7883
|
+
pesHeader[7] = 128;
|
|
7884
|
+
pesHeader[8] = 5;
|
|
7885
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
7886
|
+
return Buffer.concat([pesHeader, annexBData]);
|
|
7887
|
+
}
|
|
7888
|
+
function buildAudioPes(adtsData, ptsUs) {
|
|
7889
|
+
const pts = usToPts(ptsUs);
|
|
7890
|
+
const pesPayloadLen = 8 + adtsData.length;
|
|
7891
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
7892
|
+
pesHeader[0] = 0;
|
|
7893
|
+
pesHeader[1] = 0;
|
|
7894
|
+
pesHeader[2] = 1;
|
|
7895
|
+
pesHeader[3] = PES_STREAM_ID_AUDIO;
|
|
7896
|
+
pesHeader[4] = pesPayloadLen >> 8 & 255;
|
|
7897
|
+
pesHeader[5] = pesPayloadLen & 255;
|
|
7898
|
+
pesHeader[6] = 128;
|
|
7899
|
+
pesHeader[7] = 128;
|
|
7900
|
+
pesHeader[8] = 5;
|
|
7901
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
7902
|
+
return Buffer.concat([pesHeader, adtsData]);
|
|
7903
|
+
}
|
|
7904
|
+
var MpegTsMuxer = class {
|
|
7905
|
+
videoStreamType;
|
|
7906
|
+
includeAudio;
|
|
7907
|
+
// Per-instance continuity counters (4-bit, wrap at 16)
|
|
7908
|
+
patCc = 0;
|
|
7909
|
+
pmtCc = 0;
|
|
7910
|
+
videoCc = 0;
|
|
7911
|
+
audioCc = 0;
|
|
7912
|
+
framesSinceTableSend = 0;
|
|
7913
|
+
tablesSent = false;
|
|
7914
|
+
constructor(options) {
|
|
7915
|
+
this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
7916
|
+
this.includeAudio = options.includeAudio ?? true;
|
|
7917
|
+
}
|
|
7918
|
+
/**
|
|
7919
|
+
* Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
|
|
7920
|
+
* PAT and PMT are emitted before keyframes and periodically.
|
|
7921
|
+
*
|
|
7922
|
+
* @param annexBData - Annex-B video data (with start codes)
|
|
7923
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
7924
|
+
* @param isKeyframe - Whether this is an IDR / IRAP frame
|
|
7925
|
+
*/
|
|
7926
|
+
muxVideo(annexBData, ptsUs, isKeyframe) {
|
|
7927
|
+
const chunks = [];
|
|
7928
|
+
const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
|
|
7929
|
+
if (needTables) {
|
|
7930
|
+
chunks.push(buildPat(this.patCc));
|
|
7931
|
+
this.patCc = this.patCc + 1 & 15;
|
|
7932
|
+
chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
|
|
7933
|
+
this.pmtCc = this.pmtCc + 1 & 15;
|
|
7934
|
+
this.tablesSent = true;
|
|
7935
|
+
this.framesSinceTableSend = 0;
|
|
7936
|
+
}
|
|
7937
|
+
this.framesSinceTableSend++;
|
|
7938
|
+
const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
|
|
7939
|
+
const ccRef = { cc: this.videoCc };
|
|
7940
|
+
chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
|
|
7941
|
+
this.videoCc = ccRef.cc;
|
|
7942
|
+
return Buffer.concat(chunks);
|
|
7943
|
+
}
|
|
7944
|
+
/**
|
|
7945
|
+
* Mux an audio frame (ADTS AAC) into MPEG-TS packets.
|
|
7946
|
+
* Returns an empty Buffer when includeAudio is false.
|
|
7947
|
+
*
|
|
7948
|
+
* @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
|
|
7949
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
7950
|
+
*/
|
|
7951
|
+
muxAudio(adtsData, ptsUs) {
|
|
7952
|
+
if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
|
|
7953
|
+
const pes = buildAudioPes(adtsData, ptsUs);
|
|
7954
|
+
const ccRef = { cc: this.audioCc };
|
|
7955
|
+
const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
|
|
7956
|
+
this.audioCc = ccRef.cc;
|
|
7957
|
+
return result;
|
|
7958
|
+
}
|
|
7959
|
+
/** Reset all continuity counters and table state (e.g. after stream restart). */
|
|
7960
|
+
reset() {
|
|
7961
|
+
this.patCc = 0;
|
|
7962
|
+
this.pmtCc = 0;
|
|
7963
|
+
this.videoCc = 0;
|
|
7964
|
+
this.audioCc = 0;
|
|
7965
|
+
this.framesSinceTableSend = 0;
|
|
7966
|
+
this.tablesSent = false;
|
|
7967
|
+
}
|
|
7968
|
+
};
|
|
7969
|
+
|
|
7564
7970
|
// src/reolink/baichuan/capabilities.ts
|
|
7565
7971
|
function toNumberOrUndefined(value) {
|
|
7566
7972
|
if (value == null) return void 0;
|
|
@@ -7774,214 +8180,59 @@ function xmlIndicatesFloodlight(xml) {
|
|
|
7774
8180
|
return false;
|
|
7775
8181
|
}
|
|
7776
8182
|
|
|
8183
|
+
// src/reolink/baichuan/utils/sleepInference.ts
|
|
8184
|
+
function decideSleepInferenceTransition(input) {
|
|
8185
|
+
const { inferred, committed, pending, hysteresisPolls } = input;
|
|
8186
|
+
if (committed === void 0) {
|
|
8187
|
+
return {
|
|
8188
|
+
emit: inferred === "sleeping" ? "sleeping" : null,
|
|
8189
|
+
nextCommitted: inferred,
|
|
8190
|
+
nextPending: void 0
|
|
8191
|
+
};
|
|
8192
|
+
}
|
|
8193
|
+
if (inferred === committed) {
|
|
8194
|
+
return {
|
|
8195
|
+
emit: null,
|
|
8196
|
+
nextCommitted: committed,
|
|
8197
|
+
nextPending: void 0
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
8200
|
+
const effectivePolls = Math.max(1, hysteresisPolls);
|
|
8201
|
+
if (!pending || pending.state !== inferred) {
|
|
8202
|
+
if (effectivePolls <= 1) {
|
|
8203
|
+
return {
|
|
8204
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
8205
|
+
nextCommitted: inferred,
|
|
8206
|
+
nextPending: void 0
|
|
8207
|
+
};
|
|
8208
|
+
}
|
|
8209
|
+
return {
|
|
8210
|
+
emit: null,
|
|
8211
|
+
nextCommitted: committed,
|
|
8212
|
+
nextPending: { state: inferred, count: 1 }
|
|
8213
|
+
};
|
|
8214
|
+
}
|
|
8215
|
+
const nextCount = pending.count + 1;
|
|
8216
|
+
if (nextCount < effectivePolls) {
|
|
8217
|
+
return {
|
|
8218
|
+
emit: null,
|
|
8219
|
+
nextCommitted: committed,
|
|
8220
|
+
nextPending: { state: inferred, count: nextCount }
|
|
8221
|
+
};
|
|
8222
|
+
}
|
|
8223
|
+
return {
|
|
8224
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
8225
|
+
nextCommitted: inferred,
|
|
8226
|
+
nextPending: void 0
|
|
8227
|
+
};
|
|
8228
|
+
}
|
|
8229
|
+
|
|
7777
8230
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
7778
8231
|
import { spawn as spawn2 } from "child_process";
|
|
7779
8232
|
import { mkdir } from "fs/promises";
|
|
7780
8233
|
import { dirname } from "path";
|
|
7781
8234
|
import { PassThrough } from "stream";
|
|
7782
8235
|
|
|
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
8236
|
// src/reolink/baichuan/utils/xml.ts
|
|
7986
8237
|
import { XMLParser } from "fast-xml-parser";
|
|
7987
8238
|
var parser = new XMLParser({
|
|
@@ -10437,7 +10688,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10437
10688
|
statePollingInterval;
|
|
10438
10689
|
udpSleepInferenceInterval;
|
|
10439
10690
|
udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
10691
|
+
/**
|
|
10692
|
+
* Per-channel pending sleep-state candidate for hysteresis.
|
|
10693
|
+
* When the inference flips to a new state we require N consecutive polls
|
|
10694
|
+
* of that same state before committing it — this filters out transient
|
|
10695
|
+
* flapping caused by non-waking traffic drifting in/out of the 10 s
|
|
10696
|
+
* getSleepStatus() observation window during stream teardown.
|
|
10697
|
+
*/
|
|
10698
|
+
udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
10440
10699
|
udpSleepInferenceIntervalMs = 2e3;
|
|
10700
|
+
/** Consecutive inference polls required to commit a new sleeping/awake state. */
|
|
10701
|
+
udpSleepInferenceHysteresisPolls = 2;
|
|
10441
10702
|
lastMotionState;
|
|
10442
10703
|
lastAiState;
|
|
10443
10704
|
aiStatePollingDisabled = false;
|
|
@@ -10904,6 +11165,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
10904
11165
|
*/
|
|
10905
11166
|
attachD2cDiscListener(client) {
|
|
10906
11167
|
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
11168
|
+
client.on("error", () => {
|
|
11169
|
+
});
|
|
10907
11170
|
}
|
|
10908
11171
|
/**
|
|
10909
11172
|
* Acquire a socket from the pool by tag.
|
|
@@ -11022,6 +11285,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11022
11285
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
11023
11286
|
const newClient = new BaichuanClient(clientOpts);
|
|
11024
11287
|
this.attachD2cDiscListener(newClient);
|
|
11288
|
+
newClient.on("error", (err) => {
|
|
11289
|
+
log?.debug?.(
|
|
11290
|
+
`[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
|
|
11291
|
+
);
|
|
11292
|
+
});
|
|
11025
11293
|
await newClient.login();
|
|
11026
11294
|
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
11027
11295
|
if (existingCooldown) {
|
|
@@ -11537,6 +11805,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11537
11805
|
* Only counts sessions from our own IP address.
|
|
11538
11806
|
*/
|
|
11539
11807
|
async maybeRebootOnTooManySessions() {
|
|
11808
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
11540
11809
|
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
11541
11810
|
if (this.sessionGuardRebootInFlight) return;
|
|
11542
11811
|
const cooldownMs = 10 * 6e4;
|
|
@@ -11978,6 +12247,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
11978
12247
|
}
|
|
11979
12248
|
async renewSimpleEventSubscription() {
|
|
11980
12249
|
if (this.simpleEventListeners.size === 0) return;
|
|
12250
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
11981
12251
|
if (this.simpleEventResubscribeInFlight)
|
|
11982
12252
|
return await this.simpleEventResubscribeInFlight;
|
|
11983
12253
|
this.simpleEventResubscribeInFlight = (async () => {
|
|
@@ -15745,23 +16015,32 @@ ${stderr}`)
|
|
|
15745
16015
|
return;
|
|
15746
16016
|
}
|
|
15747
16017
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
16018
|
+
if (!this.client.isSocketConnected?.()) {
|
|
16019
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
16020
|
+
return;
|
|
16021
|
+
}
|
|
15748
16022
|
const status = this.getSleepStatus({ channel });
|
|
15749
16023
|
if (status.state === "unknown") return;
|
|
15750
|
-
const
|
|
15751
|
-
this.
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
16024
|
+
const committed = this.udpLastInferredSleepStateByChannel.get(channel);
|
|
16025
|
+
const pending = this.udpPendingSleepStateByChannel.get(channel);
|
|
16026
|
+
const decision = decideSleepInferenceTransition({
|
|
16027
|
+
inferred: status.state,
|
|
16028
|
+
committed,
|
|
16029
|
+
pending,
|
|
16030
|
+
hysteresisPolls: this.udpSleepInferenceHysteresisPolls
|
|
16031
|
+
});
|
|
16032
|
+
this.udpLastInferredSleepStateByChannel.set(
|
|
16033
|
+
channel,
|
|
16034
|
+
decision.nextCommitted
|
|
16035
|
+
);
|
|
16036
|
+
if (decision.nextPending === void 0) {
|
|
16037
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
16038
|
+
} else {
|
|
16039
|
+
this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
|
|
15761
16040
|
}
|
|
15762
|
-
if (
|
|
16041
|
+
if (decision.emit) {
|
|
15763
16042
|
this.dispatchSimpleEvent({
|
|
15764
|
-
type:
|
|
16043
|
+
type: decision.emit,
|
|
15765
16044
|
channel,
|
|
15766
16045
|
timestamp: Date.now()
|
|
15767
16046
|
});
|
|
@@ -15784,6 +16063,7 @@ ${stderr}`)
|
|
|
15784
16063
|
this.udpSleepInferenceInterval = void 0;
|
|
15785
16064
|
}
|
|
15786
16065
|
this.udpLastInferredSleepStateByChannel.clear();
|
|
16066
|
+
this.udpPendingSleepStateByChannel.clear();
|
|
15787
16067
|
}
|
|
15788
16068
|
/**
|
|
15789
16069
|
* GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
|
|
@@ -18180,7 +18460,7 @@ ${xml}`
|
|
|
18180
18460
|
* @returns Test results for all stream types and profiles
|
|
18181
18461
|
*/
|
|
18182
18462
|
async testChannelStreams(channel, logger) {
|
|
18183
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-
|
|
18463
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-HJDH4GPP.js");
|
|
18184
18464
|
return await testChannelStreams({
|
|
18185
18465
|
api: this,
|
|
18186
18466
|
channel: this.normalizeChannel(channel),
|
|
@@ -18196,7 +18476,7 @@ ${xml}`
|
|
|
18196
18476
|
* @returns Complete diagnostics for all channels and streams
|
|
18197
18477
|
*/
|
|
18198
18478
|
async collectMultifocalDiagnostics(logger) {
|
|
18199
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-
|
|
18479
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-HJDH4GPP.js");
|
|
18200
18480
|
return await collectMultifocalDiagnostics({
|
|
18201
18481
|
api: this,
|
|
18202
18482
|
logger
|
|
@@ -19382,8 +19662,8 @@ ${scheduleItems}
|
|
|
19382
19662
|
);
|
|
19383
19663
|
let args;
|
|
19384
19664
|
if (useMpegTsMuxer) {
|
|
19385
|
-
MpegTsMuxer
|
|
19386
|
-
tsMuxer
|
|
19665
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
19666
|
+
tsMuxer.reset();
|
|
19387
19667
|
args = [
|
|
19388
19668
|
"-hide_banner",
|
|
19389
19669
|
"-loglevel",
|
|
@@ -19547,7 +19827,7 @@ ${scheduleItems}
|
|
|
19547
19827
|
startFfmpeg(videoType);
|
|
19548
19828
|
frameCount++;
|
|
19549
19829
|
if (useMpegTsMuxer && tsMuxer) {
|
|
19550
|
-
const tsData = tsMuxer.
|
|
19830
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
19551
19831
|
input.write(tsData);
|
|
19552
19832
|
} else {
|
|
19553
19833
|
if (videoType === "H264") input.write(H264_AUD);
|
|
@@ -19836,8 +20116,8 @@ ${scheduleItems}
|
|
|
19836
20116
|
logger?.log?.(
|
|
19837
20117
|
`[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
|
|
19838
20118
|
);
|
|
19839
|
-
MpegTsMuxer
|
|
19840
|
-
tsMuxer
|
|
20119
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
20120
|
+
tsMuxer.reset();
|
|
19841
20121
|
const args = [
|
|
19842
20122
|
"-hide_banner",
|
|
19843
20123
|
"-loglevel",
|
|
@@ -20002,7 +20282,7 @@ ${scheduleItems}
|
|
|
20002
20282
|
startFfmpeg(videoType);
|
|
20003
20283
|
frameCount++;
|
|
20004
20284
|
if (tsMuxer) {
|
|
20005
|
-
const tsData = tsMuxer.
|
|
20285
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
20006
20286
|
input.write(tsData);
|
|
20007
20287
|
}
|
|
20008
20288
|
if (frameCount === 1) {
|
|
@@ -21414,9 +21694,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21414
21694
|
const msg = fmtErr(e);
|
|
21415
21695
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
21416
21696
|
};
|
|
21417
|
-
const withRetries = async (label, max, op, shouldRetry) => {
|
|
21697
|
+
const withRetries = async (label, max, op, shouldRetry, isAborted) => {
|
|
21418
21698
|
let lastErr;
|
|
21419
21699
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
21700
|
+
if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
|
|
21420
21701
|
try {
|
|
21421
21702
|
if (attempt > 1) {
|
|
21422
21703
|
logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
|
|
@@ -21425,7 +21706,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21425
21706
|
} catch (e) {
|
|
21426
21707
|
lastErr = e;
|
|
21427
21708
|
const msg = fmtErr(e);
|
|
21428
|
-
const retryable = attempt < max && shouldRetry(e);
|
|
21709
|
+
const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
|
|
21429
21710
|
logger?.log?.(
|
|
21430
21711
|
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
21431
21712
|
);
|
|
@@ -21435,6 +21716,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21435
21716
|
}
|
|
21436
21717
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
|
|
21437
21718
|
};
|
|
21719
|
+
const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
|
|
21720
|
+
let raceWon = false;
|
|
21721
|
+
const methodErrors = /* @__PURE__ */ new Map();
|
|
21722
|
+
try {
|
|
21723
|
+
return await Promise.any(
|
|
21724
|
+
methods.map(async (m) => {
|
|
21725
|
+
try {
|
|
21726
|
+
const result = await loginAndDetect(m, () => raceWon);
|
|
21727
|
+
raceWon = true;
|
|
21728
|
+
return result;
|
|
21729
|
+
} catch (e) {
|
|
21730
|
+
if (!raceWon) methodErrors.set(m, fmtErr(e));
|
|
21731
|
+
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
|
|
21732
|
+
throw e;
|
|
21733
|
+
}
|
|
21734
|
+
})
|
|
21735
|
+
);
|
|
21736
|
+
} catch (e) {
|
|
21737
|
+
if (e instanceof AggregateError) {
|
|
21738
|
+
const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
|
|
21739
|
+
throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
|
|
21740
|
+
}
|
|
21741
|
+
throw e;
|
|
21742
|
+
}
|
|
21743
|
+
};
|
|
21438
21744
|
const effectiveUid = normalizeUid(uid);
|
|
21439
21745
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
21440
21746
|
const isReachable = await pingHost(host);
|
|
@@ -21464,9 +21770,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21464
21770
|
normalizedUid = normalizedDiscovered;
|
|
21465
21771
|
}
|
|
21466
21772
|
const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
21773
|
+
return await runUdpMethodsParallel(
|
|
21774
|
+
methodsToTry,
|
|
21775
|
+
async (m, isAborted) => {
|
|
21470
21776
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
21471
21777
|
const udpApi = await withRetries(
|
|
21472
21778
|
`UDP(${m})`,
|
|
@@ -21489,11 +21795,14 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21489
21795
|
throw e;
|
|
21490
21796
|
}
|
|
21491
21797
|
},
|
|
21492
|
-
shouldRetryUdp
|
|
21798
|
+
shouldRetryUdp,
|
|
21799
|
+
isAborted
|
|
21493
21800
|
);
|
|
21494
|
-
const deviceInfo = await
|
|
21495
|
-
|
|
21496
|
-
|
|
21801
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
21802
|
+
udpApi.getInfo(),
|
|
21803
|
+
udpApi.getDeviceCapabilities(),
|
|
21804
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
21805
|
+
]);
|
|
21497
21806
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
21498
21807
|
const model = deviceInfo.type?.trim();
|
|
21499
21808
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -21532,14 +21841,8 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21532
21841
|
channelNum: 1,
|
|
21533
21842
|
api: udpApi
|
|
21534
21843
|
};
|
|
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(" | ")}`
|
|
21844
|
+
},
|
|
21845
|
+
"Forced UDP autodetect failed for all methods."
|
|
21543
21846
|
);
|
|
21544
21847
|
}
|
|
21545
21848
|
let tcpApi;
|
|
@@ -21592,54 +21895,57 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21592
21895
|
}
|
|
21593
21896
|
return void 0;
|
|
21594
21897
|
};
|
|
21595
|
-
const infoProbe = await
|
|
21596
|
-
|
|
21597
|
-
|
|
21898
|
+
const [infoProbe, supportProbe] = await Promise.all([
|
|
21899
|
+
runProbeVariants(
|
|
21900
|
+
"getInfo",
|
|
21901
|
+
[
|
|
21902
|
+
{
|
|
21903
|
+
variant: "cmd80 class=0x6414",
|
|
21904
|
+
op: () => api.getInfo(void 0, {
|
|
21905
|
+
timeoutMs: 2500,
|
|
21906
|
+
messageClass: BC_CLASS_MODERN_24
|
|
21907
|
+
})
|
|
21908
|
+
},
|
|
21909
|
+
{
|
|
21910
|
+
variant: "cmd80 class=0x6614",
|
|
21911
|
+
op: () => api.getInfo(void 0, {
|
|
21912
|
+
timeoutMs: 3e3,
|
|
21913
|
+
messageClass: BC_CLASS_MODERN_20
|
|
21914
|
+
})
|
|
21915
|
+
},
|
|
21916
|
+
{
|
|
21917
|
+
variant: "cmd318(ch0) class=0x6414",
|
|
21918
|
+
op: () => api.getInfo(0, {
|
|
21919
|
+
timeoutMs: 3e3,
|
|
21920
|
+
messageClass: BC_CLASS_MODERN_24
|
|
21921
|
+
})
|
|
21922
|
+
},
|
|
21923
|
+
{
|
|
21924
|
+
variant: "cmd318(ch0) class=0x6614",
|
|
21925
|
+
op: () => api.getInfo(0, {
|
|
21926
|
+
timeoutMs: 3500,
|
|
21927
|
+
messageClass: BC_CLASS_MODERN_20
|
|
21928
|
+
})
|
|
21929
|
+
}
|
|
21930
|
+
]
|
|
21931
|
+
),
|
|
21932
|
+
// Support probes (cmd 199). Some firmwares may not support it or are slow.
|
|
21933
|
+
runProbeVariants("getSupportInfo", [
|
|
21598
21934
|
{
|
|
21599
|
-
variant: "
|
|
21600
|
-
op: () => api.
|
|
21935
|
+
variant: "cmd199 class=0x6414",
|
|
21936
|
+
op: () => api.getSupportInfo({
|
|
21601
21937
|
timeoutMs: 2500,
|
|
21602
21938
|
messageClass: BC_CLASS_MODERN_24
|
|
21603
21939
|
})
|
|
21604
21940
|
},
|
|
21605
21941
|
{
|
|
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, {
|
|
21942
|
+
variant: "cmd199 class=0x6614",
|
|
21943
|
+
op: () => api.getSupportInfo({
|
|
21622
21944
|
timeoutMs: 3500,
|
|
21623
21945
|
messageClass: BC_CLASS_MODERN_20
|
|
21624
21946
|
})
|
|
21625
21947
|
}
|
|
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
|
-
}
|
|
21948
|
+
])
|
|
21643
21949
|
]);
|
|
21644
21950
|
const deviceInfo = infoProbe?.value;
|
|
21645
21951
|
const support = supportProbe?.value;
|
|
@@ -21731,9 +22037,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21731
22037
|
}
|
|
21732
22038
|
try {
|
|
21733
22039
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
21734
|
-
const deviceInfo = await
|
|
21735
|
-
|
|
21736
|
-
|
|
22040
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
22041
|
+
udpApi.getInfo(),
|
|
22042
|
+
udpApi.getDeviceCapabilities(),
|
|
22043
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
22044
|
+
]);
|
|
21737
22045
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
21738
22046
|
const model = deviceInfo.type?.trim();
|
|
21739
22047
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -21777,21 +22085,17 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21777
22085
|
};
|
|
21778
22086
|
};
|
|
21779
22087
|
const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
21780
|
-
const
|
|
21781
|
-
|
|
21782
|
-
|
|
22088
|
+
const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
|
|
22089
|
+
return await runUdpMethodsParallel(
|
|
22090
|
+
viableMethods,
|
|
22091
|
+
async (m, isAborted) => {
|
|
21783
22092
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
21784
22093
|
const udpApi = await withRetries(
|
|
21785
22094
|
`UDP(${m})`,
|
|
21786
22095
|
maxRetries,
|
|
21787
22096
|
async (attempt) => {
|
|
21788
|
-
const apiInputs = {
|
|
21789
|
-
|
|
21790
|
-
udpDiscoveryMethod: m
|
|
21791
|
-
};
|
|
21792
|
-
if (normalizedUid) {
|
|
21793
|
-
apiInputs.uid = normalizedUid;
|
|
21794
|
-
}
|
|
22097
|
+
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
22098
|
+
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
21795
22099
|
const api = createBaichuanApi(apiInputs, "udp");
|
|
21796
22100
|
try {
|
|
21797
22101
|
await api.login();
|
|
@@ -21806,20 +22110,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
21806
22110
|
throw e;
|
|
21807
22111
|
}
|
|
21808
22112
|
},
|
|
21809
|
-
shouldRetryUdp
|
|
22113
|
+
shouldRetryUdp,
|
|
22114
|
+
isAborted
|
|
21810
22115
|
);
|
|
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(" | ")}`
|
|
22116
|
+
return detectOverUdpApi(udpApi, m);
|
|
22117
|
+
},
|
|
22118
|
+
"UDP discovery failed for all methods."
|
|
21823
22119
|
);
|
|
21824
22120
|
} catch (udpError) {
|
|
21825
22121
|
logger?.log?.(
|
|
@@ -21851,12 +22147,14 @@ export {
|
|
|
21851
22147
|
BaichuanEventEmitter,
|
|
21852
22148
|
createNativeStream,
|
|
21853
22149
|
BaichuanRtspServer,
|
|
22150
|
+
MpegTsMuxer,
|
|
21854
22151
|
flattenAbilitiesForChannel,
|
|
21855
22152
|
abilitiesHasAny,
|
|
21856
22153
|
parseSupportXml,
|
|
21857
22154
|
getSupportItemForChannel,
|
|
21858
22155
|
computeDeviceCapabilities,
|
|
21859
22156
|
xmlIndicatesFloodlight,
|
|
22157
|
+
decideSleepInferenceTransition,
|
|
21860
22158
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
21861
22159
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
21862
22160
|
DUAL_LENS_MODELS,
|
|
@@ -21878,4 +22176,4 @@ export {
|
|
|
21878
22176
|
isTcpFailureThatShouldFallbackToUdp,
|
|
21879
22177
|
autoDetectDeviceType
|
|
21880
22178
|
};
|
|
21881
|
-
//# sourceMappingURL=chunk-
|
|
22179
|
+
//# sourceMappingURL=chunk-VBYF3BQX.js.map
|