@apocaliss92/nodelink-js 0.4.6 → 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-F2Y5U3YP.js → chunk-VBYF3BQX.js} +730 -357
- 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 +696 -325
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +946 -351
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -6
- package/dist/index.d.ts +283 -5
- package/dist/index.js +250 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-F2Y5U3YP.js.map +0 -1
- /package/dist/{DiagnosticsTools-UMN4C7SY.js.map → DiagnosticsTools-HJDH4GPP.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -1851,7 +1851,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1851
1851
|
const allowMsgNum0Fallback = this.acceptAnyStreamType && frame.header.msgNum === 0;
|
|
1852
1852
|
if (!allowMsgNum0Fallback) {
|
|
1853
1853
|
const frameCount = this._msgNumMismatchCount = (this._msgNumMismatchCount || 0) + 1;
|
|
1854
|
-
if (frameCount <= 5) {
|
|
1854
|
+
if (frameCount <= 5 && this.client.getDebugConfig().general) {
|
|
1855
1855
|
this.logger?.log(
|
|
1856
1856
|
`[BaichuanVideoStream] Frame msgNum mismatch: received=${frame.header.msgNum}, expected=${this.activeMsgNum}, channel=${this.channel}, profile=${this.profile}, variant=${this.variant} (frame discarded)`
|
|
1857
1857
|
);
|
|
@@ -1861,7 +1861,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1861
1861
|
}
|
|
1862
1862
|
if (!this.acceptAnyStreamType && !this.expectedStreamTypes.has(frame.header.streamType)) {
|
|
1863
1863
|
const frameCount = this._streamTypeMismatchCount = (this._streamTypeMismatchCount || 0) + 1;
|
|
1864
|
-
if (frameCount <= 5) {
|
|
1864
|
+
if (frameCount <= 5 && this.client.getDebugConfig().general) {
|
|
1865
1865
|
this.logger?.log(
|
|
1866
1866
|
`[BaichuanVideoStream] Frame streamType mismatch: received=${frame.header.streamType}, expectedAny=[${[
|
|
1867
1867
|
...this.expectedStreamTypes
|
|
@@ -7815,19 +7815,34 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
7815
7815
|
}
|
|
7816
7816
|
});
|
|
7817
7817
|
streamStarted = true;
|
|
7818
|
-
|
|
7818
|
+
const signal = options?.signal;
|
|
7819
|
+
while (!closed && !signal?.aborted) {
|
|
7819
7820
|
if (frameQueue.length > 0) {
|
|
7820
7821
|
const frame = frameQueue.shift();
|
|
7821
7822
|
yield frame;
|
|
7822
7823
|
} else {
|
|
7823
7824
|
await new Promise((resolve) => {
|
|
7824
7825
|
frameResolve = resolve;
|
|
7825
|
-
setTimeout(() => {
|
|
7826
|
+
const timer = setTimeout(() => {
|
|
7826
7827
|
if (frameResolve === resolve) {
|
|
7827
7828
|
frameResolve = null;
|
|
7828
7829
|
resolve();
|
|
7829
7830
|
}
|
|
7830
7831
|
}, 1e3);
|
|
7832
|
+
if (signal) {
|
|
7833
|
+
const onAbort = () => {
|
|
7834
|
+
clearTimeout(timer);
|
|
7835
|
+
if (frameResolve === resolve) frameResolve = null;
|
|
7836
|
+
resolve();
|
|
7837
|
+
};
|
|
7838
|
+
if (signal.aborted) {
|
|
7839
|
+
clearTimeout(timer);
|
|
7840
|
+
frameResolve = null;
|
|
7841
|
+
resolve();
|
|
7842
|
+
} else {
|
|
7843
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7844
|
+
}
|
|
7845
|
+
}
|
|
7831
7846
|
});
|
|
7832
7847
|
}
|
|
7833
7848
|
}
|
|
@@ -8000,13 +8015,14 @@ var NativeStreamFanout = class {
|
|
|
8000
8015
|
source = null;
|
|
8001
8016
|
running = false;
|
|
8002
8017
|
pumpPromise = null;
|
|
8018
|
+
abort = new AbortController();
|
|
8003
8019
|
constructor(opts) {
|
|
8004
8020
|
this.opts = opts;
|
|
8005
8021
|
}
|
|
8006
8022
|
start() {
|
|
8007
8023
|
if (this.running) return;
|
|
8008
8024
|
this.running = true;
|
|
8009
|
-
this.source = this.opts.createSource();
|
|
8025
|
+
this.source = this.opts.createSource(this.abort.signal);
|
|
8010
8026
|
this.pumpPromise = (async () => {
|
|
8011
8027
|
try {
|
|
8012
8028
|
for await (const frame of this.source) {
|
|
@@ -8052,6 +8068,7 @@ var NativeStreamFanout = class {
|
|
|
8052
8068
|
this.source = null;
|
|
8053
8069
|
for (const q of this.queues.values()) q.close();
|
|
8054
8070
|
this.queues.clear();
|
|
8071
|
+
this.abort.abort();
|
|
8055
8072
|
try {
|
|
8056
8073
|
await src?.return(void 0);
|
|
8057
8074
|
} catch {
|
|
@@ -8090,9 +8107,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8090
8107
|
requireAuth;
|
|
8091
8108
|
authNonces = /* @__PURE__ */ new Map();
|
|
8092
8109
|
// Track nonces per client
|
|
8093
|
-
AUTH_REALM
|
|
8110
|
+
AUTH_REALM;
|
|
8094
8111
|
NONCE_TIMEOUT_MS = 3e5;
|
|
8095
8112
|
// 5 minutes
|
|
8113
|
+
lazyMetadata;
|
|
8096
8114
|
// Client tracking
|
|
8097
8115
|
connectedClients = /* @__PURE__ */ new Set();
|
|
8098
8116
|
// Set of client IDs (IP:port)
|
|
@@ -8104,8 +8122,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8104
8122
|
// Track all client resources for cleanup
|
|
8105
8123
|
clientResources = /* @__PURE__ */ new Map();
|
|
8106
8124
|
isRtspDebugEnabled() {
|
|
8107
|
-
|
|
8108
|
-
|
|
8125
|
+
try {
|
|
8126
|
+
if (this.api.isClosed) {
|
|
8127
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
8128
|
+
}
|
|
8129
|
+
const dbg = this.api.client.getDebugConfig();
|
|
8130
|
+
return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
8131
|
+
} catch {
|
|
8132
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
8133
|
+
}
|
|
8109
8134
|
}
|
|
8110
8135
|
rtspDebugLog(message) {
|
|
8111
8136
|
if (!this.isRtspDebugEnabled()) return;
|
|
@@ -8127,10 +8152,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8127
8152
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
8128
8153
|
nativeFanout = null;
|
|
8129
8154
|
noClientAutoStopTimer;
|
|
8155
|
+
/** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
|
|
8156
|
+
noFrameDeadlineTimer;
|
|
8130
8157
|
/** After last RTSP client; 0 = never auto-stop native stream. */
|
|
8131
8158
|
nativeStreamIdleStopMs;
|
|
8132
8159
|
/** Primed-but-no-PLAY timeout; 0 = disabled. */
|
|
8133
8160
|
nativeStreamPrimeIdleStopMs;
|
|
8161
|
+
/**
|
|
8162
|
+
* Max time to wait for the first camera frame after stream start.
|
|
8163
|
+
* If no frames arrive within this window, the native stream is stopped
|
|
8164
|
+
* (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
|
|
8165
|
+
* firing and waking the camera when no real viewer is watching.
|
|
8166
|
+
* 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
|
|
8167
|
+
*/
|
|
8168
|
+
nativeStreamNoFrameDeadlineMs;
|
|
8134
8169
|
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
8135
8170
|
// When a new client connects while the stream is already running it does not need
|
|
8136
8171
|
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
@@ -8256,14 +8291,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8256
8291
|
this.logger = options.logger ?? console;
|
|
8257
8292
|
this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
|
|
8258
8293
|
this.deviceId = options.deviceId;
|
|
8259
|
-
this.externalListener = options.externalListener ?? false;
|
|
8294
|
+
this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
|
|
8260
8295
|
this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
|
|
8261
8296
|
this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
|
|
8262
|
-
this.
|
|
8297
|
+
this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
|
|
8298
|
+
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
8299
|
+
username: c.username,
|
|
8300
|
+
...c.password !== void 0 ? { password: c.password } : {},
|
|
8301
|
+
...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
|
|
8302
|
+
}));
|
|
8263
8303
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
8304
|
+
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
8305
|
+
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
8264
8306
|
const transport = this.api.client.getTransport();
|
|
8265
8307
|
this.flow = createRtspFlow(transport, "H264");
|
|
8266
8308
|
}
|
|
8309
|
+
/** Number of currently connected RTSP clients. */
|
|
8310
|
+
get clientCount() {
|
|
8311
|
+
return this.connectedClients.size;
|
|
8312
|
+
}
|
|
8267
8313
|
// --- Authentication helpers ---
|
|
8268
8314
|
/**
|
|
8269
8315
|
* Generate a new nonce for Digest authentication
|
|
@@ -8324,9 +8370,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8324
8370
|
this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
|
|
8325
8371
|
return false;
|
|
8326
8372
|
}
|
|
8373
|
+
if (realm !== this.AUTH_REALM) {
|
|
8374
|
+
this.rtspDebugLog(
|
|
8375
|
+
`Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
|
|
8376
|
+
);
|
|
8377
|
+
return false;
|
|
8378
|
+
}
|
|
8327
8379
|
for (const cred of this.authCredentials) {
|
|
8328
8380
|
if (username !== cred.username) continue;
|
|
8329
|
-
const ha1 = this.md5(`${cred.username}:${
|
|
8381
|
+
const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
|
|
8382
|
+
if (!ha1) continue;
|
|
8330
8383
|
const ha2 = this.md5(`${method}:${authUri || uri}`);
|
|
8331
8384
|
const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
|
|
8332
8385
|
if (response === expectedResponse) {
|
|
@@ -8355,6 +8408,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8355
8408
|
this.noClientAutoStopTimer = void 0;
|
|
8356
8409
|
}
|
|
8357
8410
|
}
|
|
8411
|
+
clearNoFrameDeadlineTimer() {
|
|
8412
|
+
if (this.noFrameDeadlineTimer) {
|
|
8413
|
+
clearTimeout(this.noFrameDeadlineTimer);
|
|
8414
|
+
this.noFrameDeadlineTimer = void 0;
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
8358
8417
|
setFlowVideoType(videoType, reason) {
|
|
8359
8418
|
if (this.flow.videoType === videoType) return;
|
|
8360
8419
|
const transport = this.api.client.getTransport();
|
|
@@ -8369,25 +8428,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8369
8428
|
if (this.active) {
|
|
8370
8429
|
throw new Error("RTSP server is already active");
|
|
8371
8430
|
}
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
if (stream) {
|
|
8376
|
-
this.streamMetadata = {
|
|
8377
|
-
frameRate: stream.frameRate || 25,
|
|
8378
|
-
width: stream.width,
|
|
8379
|
-
height: stream.height
|
|
8380
|
-
};
|
|
8381
|
-
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
8382
|
-
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
8383
|
-
this.setFlowVideoType(metaVideoType, "metadata");
|
|
8384
|
-
}
|
|
8385
|
-
} catch (error) {
|
|
8386
|
-
this.logger.warn(
|
|
8387
|
-
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
8431
|
+
if (this.lazyMetadata) {
|
|
8432
|
+
this.logger.info(
|
|
8433
|
+
`[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
|
|
8388
8434
|
);
|
|
8389
|
-
|
|
8390
|
-
|
|
8435
|
+
} else {
|
|
8436
|
+
try {
|
|
8437
|
+
const metadata = await this.api.getStreamMetadata(this.channel);
|
|
8438
|
+
const stream = metadata.streams.find((s) => s.profile === this.profile);
|
|
8439
|
+
if (stream) {
|
|
8440
|
+
this.streamMetadata = {
|
|
8441
|
+
frameRate: stream.frameRate || 25,
|
|
8442
|
+
width: stream.width,
|
|
8443
|
+
height: stream.height
|
|
8444
|
+
};
|
|
8445
|
+
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
8446
|
+
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
8447
|
+
this.setFlowVideoType(metaVideoType, "metadata");
|
|
8448
|
+
}
|
|
8449
|
+
} catch (error) {
|
|
8450
|
+
this.logger.warn(
|
|
8451
|
+
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
8452
|
+
);
|
|
8453
|
+
this.streamMetadata = { frameRate: 25 };
|
|
8454
|
+
this.setFlowVideoType("H264", "metadata unavailable");
|
|
8455
|
+
}
|
|
8391
8456
|
}
|
|
8392
8457
|
if (!this.externalListener) {
|
|
8393
8458
|
this.clientConnectionServer = net.createServer((socket) => {
|
|
@@ -8428,6 +8493,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8428
8493
|
}
|
|
8429
8494
|
this.handleRtspConnection(socket, initialBuffer);
|
|
8430
8495
|
}
|
|
8496
|
+
/**
|
|
8497
|
+
* Inject an already-accepted client socket from a multiplexer
|
|
8498
|
+
* (e.g. `LocalRtspMux`) that owns the listening port.
|
|
8499
|
+
*
|
|
8500
|
+
* The mux reads the first RTSP request line to determine the target path,
|
|
8501
|
+
* then hands the socket over. Any bytes already consumed during routing
|
|
8502
|
+
* are replayed back onto the socket via `unshift()` so the RTSP parser in
|
|
8503
|
+
* `handleRtspConnection` sees the complete original request.
|
|
8504
|
+
*
|
|
8505
|
+
* @param socket - Client TCP socket, already accepted by the mux.
|
|
8506
|
+
* @param preReadData - Bytes the mux has already pulled off the socket
|
|
8507
|
+
* while parsing the request line. Replayed via `socket.unshift()`
|
|
8508
|
+
* before any further reads.
|
|
8509
|
+
*/
|
|
8510
|
+
injectSocket(socket, preReadData) {
|
|
8511
|
+
if (!this.active) {
|
|
8512
|
+
socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
|
|
8513
|
+
return;
|
|
8514
|
+
}
|
|
8515
|
+
if (preReadData && preReadData.length > 0) {
|
|
8516
|
+
socket.unshift(preReadData);
|
|
8517
|
+
}
|
|
8518
|
+
this.handleRtspConnection(socket);
|
|
8519
|
+
}
|
|
8431
8520
|
/**
|
|
8432
8521
|
* Handle RTSP connection from a client.
|
|
8433
8522
|
*/
|
|
@@ -8592,6 +8681,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8592
8681
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
8593
8682
|
});
|
|
8594
8683
|
} else if (method === "DESCRIBE") {
|
|
8684
|
+
if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
|
|
8685
|
+
void this.api.ensureConnected().catch(() => {
|
|
8686
|
+
});
|
|
8687
|
+
}
|
|
8595
8688
|
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
8596
8689
|
try {
|
|
8597
8690
|
if (!this.nativeStreamActive) {
|
|
@@ -8629,6 +8722,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8629
8722
|
}
|
|
8630
8723
|
}
|
|
8631
8724
|
}
|
|
8725
|
+
if (!this.hasAudio && this.firstAudioPromise) {
|
|
8726
|
+
const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
|
|
8727
|
+
const audioPrimingStart = Date.now();
|
|
8728
|
+
try {
|
|
8729
|
+
await Promise.race([
|
|
8730
|
+
this.firstAudioPromise,
|
|
8731
|
+
new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
|
|
8732
|
+
]);
|
|
8733
|
+
} catch {
|
|
8734
|
+
}
|
|
8735
|
+
const audioPrimingElapsed = Date.now() - audioPrimingStart;
|
|
8736
|
+
if (this.hasAudio) {
|
|
8737
|
+
this.logger.info(
|
|
8738
|
+
`[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
|
|
8739
|
+
);
|
|
8740
|
+
} else {
|
|
8741
|
+
this.logger.info(
|
|
8742
|
+
`[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
|
|
8743
|
+
);
|
|
8744
|
+
}
|
|
8745
|
+
}
|
|
8632
8746
|
{
|
|
8633
8747
|
const { fmtp, hasParamSets } = this.flow.getFmtp();
|
|
8634
8748
|
const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
|
|
@@ -8637,12 +8751,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8637
8751
|
);
|
|
8638
8752
|
}
|
|
8639
8753
|
const sdp = this.generateSdp();
|
|
8754
|
+
const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
|
|
8640
8755
|
sendResponse(
|
|
8641
8756
|
200,
|
|
8642
8757
|
"OK",
|
|
8643
8758
|
{
|
|
8644
8759
|
"Content-Type": "application/sdp",
|
|
8645
|
-
"Content-Base": `rtsp://${
|
|
8760
|
+
"Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
|
|
8646
8761
|
},
|
|
8647
8762
|
sdp
|
|
8648
8763
|
);
|
|
@@ -8665,7 +8780,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8665
8780
|
this.emit("client", clientId);
|
|
8666
8781
|
this.clearNoClientAutoStopTimer();
|
|
8667
8782
|
if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
|
|
8668
|
-
|
|
8783
|
+
void this.startNativeStream();
|
|
8669
8784
|
}
|
|
8670
8785
|
const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
|
|
8671
8786
|
const transport = (transportMatch?.[1] ?? "").trim();
|
|
@@ -8799,12 +8914,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8799
8914
|
}
|
|
8800
8915
|
}
|
|
8801
8916
|
};
|
|
8917
|
+
const runProcessBuffer = () => {
|
|
8918
|
+
processBuffer().catch((err) => {
|
|
8919
|
+
this.logger.debug(
|
|
8920
|
+
`[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
|
|
8921
|
+
);
|
|
8922
|
+
try {
|
|
8923
|
+
socket.destroy();
|
|
8924
|
+
} catch {
|
|
8925
|
+
}
|
|
8926
|
+
});
|
|
8927
|
+
};
|
|
8802
8928
|
socket.on("data", (data) => {
|
|
8803
8929
|
buffer = Buffer.concat([buffer, data]);
|
|
8804
|
-
|
|
8930
|
+
runProcessBuffer();
|
|
8805
8931
|
});
|
|
8806
8932
|
if (buffer.includes("\r\n\r\n")) {
|
|
8807
|
-
|
|
8933
|
+
runProcessBuffer();
|
|
8808
8934
|
}
|
|
8809
8935
|
}
|
|
8810
8936
|
/**
|
|
@@ -9672,6 +9798,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9672
9798
|
if (this.nativeStreamActive) {
|
|
9673
9799
|
return;
|
|
9674
9800
|
}
|
|
9801
|
+
if (!this.api.isReady) {
|
|
9802
|
+
if (this.api.isClosed) {
|
|
9803
|
+
this.logger.warn?.(
|
|
9804
|
+
`[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
|
|
9805
|
+
);
|
|
9806
|
+
return;
|
|
9807
|
+
}
|
|
9808
|
+
try {
|
|
9809
|
+
this.logger.info?.(
|
|
9810
|
+
`[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
|
|
9811
|
+
);
|
|
9812
|
+
await this.api.ensureConnected();
|
|
9813
|
+
} catch (e) {
|
|
9814
|
+
this.logger.warn?.(
|
|
9815
|
+
`[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
|
|
9816
|
+
);
|
|
9817
|
+
return;
|
|
9818
|
+
}
|
|
9819
|
+
}
|
|
9675
9820
|
this.nativeStreamActive = true;
|
|
9676
9821
|
this.firstFrameReceived = false;
|
|
9677
9822
|
this.firstAudioDetected = false;
|
|
@@ -9706,13 +9851,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9706
9851
|
await this.flow.startKeepAlive(this.api);
|
|
9707
9852
|
this.nativeFanout = new NativeStreamFanout({
|
|
9708
9853
|
maxQueueItems: 200,
|
|
9709
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
9854
|
+
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
9710
9855
|
variant: this.variant,
|
|
9711
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
9856
|
+
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
9857
|
+
signal
|
|
9712
9858
|
}),
|
|
9713
9859
|
onFrame: (frame) => {
|
|
9714
9860
|
if (frame.audio) {
|
|
9715
|
-
if (!this.hasAudio &&
|
|
9861
|
+
if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
|
|
9716
9862
|
const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
|
|
9717
9863
|
if (info) {
|
|
9718
9864
|
this.hasAudio = true;
|
|
@@ -9761,6 +9907,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9761
9907
|
onEnd: () => {
|
|
9762
9908
|
if (!this.nativeStreamActive) return;
|
|
9763
9909
|
this.nativeStreamActive = false;
|
|
9910
|
+
this.clearNoFrameDeadlineTimer();
|
|
9911
|
+
const hadFrames = this.firstFrameReceived;
|
|
9764
9912
|
this.firstFrameReceived = false;
|
|
9765
9913
|
this.firstFramePromise = null;
|
|
9766
9914
|
this.firstFrameResolve = null;
|
|
@@ -9785,7 +9933,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9785
9933
|
} catch {
|
|
9786
9934
|
}
|
|
9787
9935
|
}
|
|
9788
|
-
if (this.connectedClients.size > 0) {
|
|
9936
|
+
if (this.connectedClients.size > 0 && hadFrames) {
|
|
9789
9937
|
this.logger.info(
|
|
9790
9938
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
9791
9939
|
);
|
|
@@ -9797,6 +9945,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9797
9945
|
}
|
|
9798
9946
|
});
|
|
9799
9947
|
this.nativeFanout.start();
|
|
9948
|
+
this.clearNoFrameDeadlineTimer();
|
|
9949
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
9950
|
+
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
9951
|
+
this.noFrameDeadlineTimer = void 0;
|
|
9952
|
+
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
9953
|
+
this.logger.info(
|
|
9954
|
+
`[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
|
|
9955
|
+
);
|
|
9956
|
+
void this.stopNativeStream();
|
|
9957
|
+
}
|
|
9958
|
+
}, this.nativeStreamNoFrameDeadlineMs);
|
|
9959
|
+
this.noFrameDeadlineTimer?.unref?.();
|
|
9960
|
+
}
|
|
9800
9961
|
this.clearNoClientAutoStopTimer();
|
|
9801
9962
|
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
9802
9963
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
@@ -9813,6 +9974,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9813
9974
|
markFirstFrameReceived() {
|
|
9814
9975
|
if (!this.firstFrameReceived && this.firstFrameResolve) {
|
|
9815
9976
|
this.firstFrameReceived = true;
|
|
9977
|
+
this.clearNoFrameDeadlineTimer();
|
|
9816
9978
|
this.rtspDebugLog(
|
|
9817
9979
|
`First frame received from camera for profile ${this.profile}`
|
|
9818
9980
|
);
|
|
@@ -9839,6 +10001,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9839
10001
|
);
|
|
9840
10002
|
this.flow.stopKeepAlive();
|
|
9841
10003
|
this.clearNoClientAutoStopTimer();
|
|
10004
|
+
this.clearNoFrameDeadlineTimer();
|
|
9842
10005
|
this.nativeStreamActive = false;
|
|
9843
10006
|
this.firstFrameReceived = false;
|
|
9844
10007
|
this.firstFramePromise = null;
|
|
@@ -10055,149 +10218,17 @@ init_BcMediaAnnexBDecoder();
|
|
|
10055
10218
|
// src/baichuan/stream/MpegTsMuxer.ts
|
|
10056
10219
|
var TS_PACKET_SIZE = 188;
|
|
10057
10220
|
var TS_SYNC_BYTE = 71;
|
|
10058
|
-
var
|
|
10059
|
-
var
|
|
10060
|
-
var
|
|
10221
|
+
var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
|
|
10222
|
+
var PID_PAT = 0;
|
|
10223
|
+
var PID_PMT = 4096;
|
|
10224
|
+
var PID_VIDEO = 256;
|
|
10225
|
+
var PID_AUDIO = 257;
|
|
10061
10226
|
var STREAM_TYPE_H264 = 27;
|
|
10062
10227
|
var STREAM_TYPE_H265 = 36;
|
|
10063
|
-
var
|
|
10064
|
-
var
|
|
10065
|
-
var
|
|
10066
|
-
|
|
10067
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10068
|
-
packet[0] = TS_SYNC_BYTE;
|
|
10069
|
-
packet[1] = 64 | PAT_PID >> 8 & 31;
|
|
10070
|
-
packet[2] = PAT_PID & 255;
|
|
10071
|
-
packet[3] = 16 | patCc & 15;
|
|
10072
|
-
patCc = patCc + 1 & 15;
|
|
10073
|
-
packet[4] = 0;
|
|
10074
|
-
let idx = 5;
|
|
10075
|
-
packet[idx++] = 0;
|
|
10076
|
-
packet[idx++] = 176;
|
|
10077
|
-
packet[idx++] = 13;
|
|
10078
|
-
packet[idx++] = 0;
|
|
10079
|
-
packet[idx++] = 1;
|
|
10080
|
-
packet[idx++] = 193;
|
|
10081
|
-
packet[idx++] = 0;
|
|
10082
|
-
packet[idx++] = 0;
|
|
10083
|
-
packet[idx++] = 0;
|
|
10084
|
-
packet[idx++] = 1;
|
|
10085
|
-
packet[idx++] = 224 | PMT_PID >> 8 & 31;
|
|
10086
|
-
packet[idx++] = PMT_PID & 255;
|
|
10087
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
10088
|
-
packet.writeUInt32BE(crc, idx);
|
|
10089
|
-
return packet;
|
|
10090
|
-
}
|
|
10091
|
-
function createPmt(streamType) {
|
|
10092
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10093
|
-
packet[0] = TS_SYNC_BYTE;
|
|
10094
|
-
packet[1] = 64 | PMT_PID >> 8 & 31;
|
|
10095
|
-
packet[2] = PMT_PID & 255;
|
|
10096
|
-
packet[3] = 16 | pmtCc & 15;
|
|
10097
|
-
pmtCc = pmtCc + 1 & 15;
|
|
10098
|
-
packet[4] = 0;
|
|
10099
|
-
let idx = 5;
|
|
10100
|
-
packet[idx++] = 2;
|
|
10101
|
-
packet[idx++] = 176;
|
|
10102
|
-
packet[idx++] = 18;
|
|
10103
|
-
packet[idx++] = 0;
|
|
10104
|
-
packet[idx++] = 1;
|
|
10105
|
-
packet[idx++] = 193;
|
|
10106
|
-
packet[idx++] = 0;
|
|
10107
|
-
packet[idx++] = 0;
|
|
10108
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
10109
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
10110
|
-
packet[idx++] = 240;
|
|
10111
|
-
packet[idx++] = 0;
|
|
10112
|
-
packet[idx++] = streamType;
|
|
10113
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
10114
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
10115
|
-
packet[idx++] = 240;
|
|
10116
|
-
packet[idx++] = 0;
|
|
10117
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
10118
|
-
packet.writeUInt32BE(crc, idx);
|
|
10119
|
-
return packet;
|
|
10120
|
-
}
|
|
10121
|
-
function createVideoPes(data, pts, isKeyframe) {
|
|
10122
|
-
const packets = [];
|
|
10123
|
-
const pts90k = Math.floor(pts * 9e4 / 1e6);
|
|
10124
|
-
const pesHeaderLen = 14;
|
|
10125
|
-
const pesHeader = Buffer.alloc(pesHeaderLen);
|
|
10126
|
-
let idx = 0;
|
|
10127
|
-
pesHeader[idx++] = 0;
|
|
10128
|
-
pesHeader[idx++] = 0;
|
|
10129
|
-
pesHeader[idx++] = 1;
|
|
10130
|
-
pesHeader[idx++] = 224;
|
|
10131
|
-
pesHeader[idx++] = 0;
|
|
10132
|
-
pesHeader[idx++] = 0;
|
|
10133
|
-
pesHeader[idx++] = 128;
|
|
10134
|
-
pesHeader[idx++] = 128;
|
|
10135
|
-
pesHeader[idx++] = 5;
|
|
10136
|
-
pesHeader[idx++] = 33 | pts90k >> 29 & 14;
|
|
10137
|
-
pesHeader[idx++] = pts90k >> 22 & 255;
|
|
10138
|
-
pesHeader[idx++] = 1 | pts90k >> 14 & 254;
|
|
10139
|
-
pesHeader[idx++] = pts90k >> 7 & 255;
|
|
10140
|
-
pesHeader[idx++] = 1 | pts90k << 1 & 254;
|
|
10141
|
-
const pesData = Buffer.concat([pesHeader, data]);
|
|
10142
|
-
let pesOffset = 0;
|
|
10143
|
-
let isFirst = true;
|
|
10144
|
-
while (pesOffset < pesData.length) {
|
|
10145
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10146
|
-
let pktIdx = 0;
|
|
10147
|
-
packet[pktIdx++] = TS_SYNC_BYTE;
|
|
10148
|
-
packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
|
|
10149
|
-
packet[pktIdx++] = VIDEO_PID & 255;
|
|
10150
|
-
const remaining = pesData.length - pesOffset;
|
|
10151
|
-
const maxPayload = TS_PACKET_SIZE - 4;
|
|
10152
|
-
if (remaining >= maxPayload) {
|
|
10153
|
-
packet[pktIdx++] = 16 | videoCc & 15;
|
|
10154
|
-
videoCc = videoCc + 1 & 15;
|
|
10155
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
|
|
10156
|
-
pesOffset += maxPayload;
|
|
10157
|
-
} else {
|
|
10158
|
-
const adaptLen = maxPayload - remaining - 1;
|
|
10159
|
-
if (adaptLen < 0) {
|
|
10160
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
10161
|
-
videoCc = videoCc + 1 & 15;
|
|
10162
|
-
packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
|
|
10163
|
-
if (isFirst && isKeyframe) {
|
|
10164
|
-
packet[pktIdx++] = 64;
|
|
10165
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
10166
|
-
packet[i] = 255;
|
|
10167
|
-
}
|
|
10168
|
-
} else {
|
|
10169
|
-
packet[pktIdx++] = 0;
|
|
10170
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
10171
|
-
packet[i] = 255;
|
|
10172
|
-
}
|
|
10173
|
-
}
|
|
10174
|
-
pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
|
|
10175
|
-
pesOffset += remaining;
|
|
10176
|
-
} else {
|
|
10177
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
10178
|
-
videoCc = videoCc + 1 & 15;
|
|
10179
|
-
if (adaptLen === 0) {
|
|
10180
|
-
packet[pktIdx++] = 0;
|
|
10181
|
-
} else {
|
|
10182
|
-
packet[pktIdx++] = adaptLen;
|
|
10183
|
-
if (isFirst && isKeyframe) {
|
|
10184
|
-
packet[pktIdx++] = 64;
|
|
10185
|
-
} else {
|
|
10186
|
-
packet[pktIdx++] = 0;
|
|
10187
|
-
}
|
|
10188
|
-
for (let i = 0; i < adaptLen - 1; i++) {
|
|
10189
|
-
packet[pktIdx++] = 255;
|
|
10190
|
-
}
|
|
10191
|
-
}
|
|
10192
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
|
|
10193
|
-
pesOffset += remaining;
|
|
10194
|
-
}
|
|
10195
|
-
}
|
|
10196
|
-
packets.push(packet);
|
|
10197
|
-
isFirst = false;
|
|
10198
|
-
}
|
|
10199
|
-
return packets;
|
|
10200
|
-
}
|
|
10228
|
+
var STREAM_TYPE_AAC = 15;
|
|
10229
|
+
var PES_STREAM_ID_VIDEO = 224;
|
|
10230
|
+
var PES_STREAM_ID_AUDIO = 192;
|
|
10231
|
+
var PAT_PMT_INTERVAL = 40;
|
|
10201
10232
|
function crc32Mpeg(data) {
|
|
10202
10233
|
let crc = 4294967295;
|
|
10203
10234
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -10212,45 +10243,218 @@ function crc32Mpeg(data) {
|
|
|
10212
10243
|
}
|
|
10213
10244
|
return crc >>> 0;
|
|
10214
10245
|
}
|
|
10246
|
+
function usToPts(us) {
|
|
10247
|
+
return Math.floor(us * 90 / 1e3) & 8589934591;
|
|
10248
|
+
}
|
|
10249
|
+
function encodePts(buf, offset, pts, prefix) {
|
|
10250
|
+
buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
|
|
10251
|
+
buf[offset + 1] = pts >>> 22 & 255;
|
|
10252
|
+
buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
|
|
10253
|
+
buf[offset + 3] = pts >>> 7 & 255;
|
|
10254
|
+
buf[offset + 4] = (pts & 127) << 1 | 1;
|
|
10255
|
+
}
|
|
10256
|
+
function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
|
|
10257
|
+
buf[0] = TS_SYNC_BYTE;
|
|
10258
|
+
buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
|
|
10259
|
+
buf[2] = pid & 255;
|
|
10260
|
+
buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
|
|
10261
|
+
}
|
|
10262
|
+
function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
|
|
10263
|
+
const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
|
|
10264
|
+
const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
|
|
10265
|
+
let pesOffset = 0;
|
|
10266
|
+
let outOffset = 0;
|
|
10267
|
+
let isFirst = true;
|
|
10268
|
+
while (pesOffset < pesData.length) {
|
|
10269
|
+
const remaining = pesData.length - pesOffset;
|
|
10270
|
+
const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
|
|
10271
|
+
outOffset += TS_PACKET_SIZE;
|
|
10272
|
+
if (remaining >= TS_PAYLOAD_SIZE) {
|
|
10273
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
|
|
10274
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
10275
|
+
pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
|
|
10276
|
+
pesOffset += TS_PAYLOAD_SIZE;
|
|
10277
|
+
} else {
|
|
10278
|
+
const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
|
|
10279
|
+
if (paddingNeeded === 1) {
|
|
10280
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
10281
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
10282
|
+
packet[4] = 0;
|
|
10283
|
+
pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
|
|
10284
|
+
} else {
|
|
10285
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
10286
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
10287
|
+
const adaptLen = paddingNeeded - 1;
|
|
10288
|
+
packet[4] = adaptLen;
|
|
10289
|
+
packet[5] = isFirst && isKeyframe ? 64 : 0;
|
|
10290
|
+
packet.fill(255, 6, 4 + paddingNeeded);
|
|
10291
|
+
pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
|
|
10292
|
+
}
|
|
10293
|
+
pesOffset += remaining;
|
|
10294
|
+
}
|
|
10295
|
+
isFirst = false;
|
|
10296
|
+
}
|
|
10297
|
+
return out;
|
|
10298
|
+
}
|
|
10299
|
+
function buildPat(cc) {
|
|
10300
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10301
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
10302
|
+
pkt[1] = 64 | PID_PAT >> 8 & 31;
|
|
10303
|
+
pkt[2] = PID_PAT & 255;
|
|
10304
|
+
pkt[3] = 16 | cc & 15;
|
|
10305
|
+
pkt[4] = 0;
|
|
10306
|
+
const sectionStart = 5;
|
|
10307
|
+
let i = sectionStart;
|
|
10308
|
+
pkt[i++] = 0;
|
|
10309
|
+
pkt[i++] = 176;
|
|
10310
|
+
pkt[i++] = 13;
|
|
10311
|
+
pkt[i++] = 0;
|
|
10312
|
+
pkt[i++] = 1;
|
|
10313
|
+
pkt[i++] = 193;
|
|
10314
|
+
pkt[i++] = 0;
|
|
10315
|
+
pkt[i++] = 0;
|
|
10316
|
+
pkt[i++] = 0;
|
|
10317
|
+
pkt[i++] = 1;
|
|
10318
|
+
pkt[i++] = 224 | PID_PMT >> 8 & 31;
|
|
10319
|
+
pkt[i++] = PID_PMT & 255;
|
|
10320
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
10321
|
+
pkt.writeUInt32BE(crc, i);
|
|
10322
|
+
return pkt;
|
|
10323
|
+
}
|
|
10324
|
+
function buildPmt(videoStreamType, includeAudio, cc) {
|
|
10325
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10326
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
10327
|
+
pkt[1] = 64 | PID_PMT >> 8 & 31;
|
|
10328
|
+
pkt[2] = PID_PMT & 255;
|
|
10329
|
+
pkt[3] = 16 | cc & 15;
|
|
10330
|
+
pkt[4] = 0;
|
|
10331
|
+
const sectionStart = 5;
|
|
10332
|
+
let i = sectionStart;
|
|
10333
|
+
pkt[i++] = 2;
|
|
10334
|
+
pkt[i++] = 176;
|
|
10335
|
+
const sectionLenPos = i;
|
|
10336
|
+
i += 1;
|
|
10337
|
+
pkt[i++] = 0;
|
|
10338
|
+
pkt[i++] = 1;
|
|
10339
|
+
pkt[i++] = 193;
|
|
10340
|
+
pkt[i++] = 0;
|
|
10341
|
+
pkt[i++] = 0;
|
|
10342
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
10343
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
10344
|
+
pkt[i++] = 240;
|
|
10345
|
+
pkt[i++] = 0;
|
|
10346
|
+
pkt[i++] = videoStreamType;
|
|
10347
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
10348
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
10349
|
+
pkt[i++] = 240;
|
|
10350
|
+
pkt[i++] = 0;
|
|
10351
|
+
if (includeAudio) {
|
|
10352
|
+
pkt[i++] = STREAM_TYPE_AAC;
|
|
10353
|
+
pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
|
|
10354
|
+
pkt[i++] = PID_AUDIO & 255;
|
|
10355
|
+
pkt[i++] = 240;
|
|
10356
|
+
pkt[i++] = 0;
|
|
10357
|
+
}
|
|
10358
|
+
const sectionLen = i - sectionStart - 3 + 4;
|
|
10359
|
+
pkt[sectionLenPos] = sectionLen;
|
|
10360
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
10361
|
+
pkt.writeUInt32BE(crc, i);
|
|
10362
|
+
return pkt;
|
|
10363
|
+
}
|
|
10364
|
+
function buildVideoPes(annexBData, ptsUs, isKeyframe) {
|
|
10365
|
+
const pts = usToPts(ptsUs);
|
|
10366
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
10367
|
+
pesHeader[0] = 0;
|
|
10368
|
+
pesHeader[1] = 0;
|
|
10369
|
+
pesHeader[2] = 1;
|
|
10370
|
+
pesHeader[3] = PES_STREAM_ID_VIDEO;
|
|
10371
|
+
pesHeader[4] = 0;
|
|
10372
|
+
pesHeader[5] = 0;
|
|
10373
|
+
pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
|
|
10374
|
+
pesHeader[7] = 128;
|
|
10375
|
+
pesHeader[8] = 5;
|
|
10376
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
10377
|
+
return Buffer.concat([pesHeader, annexBData]);
|
|
10378
|
+
}
|
|
10379
|
+
function buildAudioPes(adtsData, ptsUs) {
|
|
10380
|
+
const pts = usToPts(ptsUs);
|
|
10381
|
+
const pesPayloadLen = 8 + adtsData.length;
|
|
10382
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
10383
|
+
pesHeader[0] = 0;
|
|
10384
|
+
pesHeader[1] = 0;
|
|
10385
|
+
pesHeader[2] = 1;
|
|
10386
|
+
pesHeader[3] = PES_STREAM_ID_AUDIO;
|
|
10387
|
+
pesHeader[4] = pesPayloadLen >> 8 & 255;
|
|
10388
|
+
pesHeader[5] = pesPayloadLen & 255;
|
|
10389
|
+
pesHeader[6] = 128;
|
|
10390
|
+
pesHeader[7] = 128;
|
|
10391
|
+
pesHeader[8] = 5;
|
|
10392
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
10393
|
+
return Buffer.concat([pesHeader, adtsData]);
|
|
10394
|
+
}
|
|
10215
10395
|
var MpegTsMuxer = class {
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10396
|
+
videoStreamType;
|
|
10397
|
+
includeAudio;
|
|
10398
|
+
// Per-instance continuity counters (4-bit, wrap at 16)
|
|
10399
|
+
patCc = 0;
|
|
10400
|
+
pmtCc = 0;
|
|
10401
|
+
videoCc = 0;
|
|
10402
|
+
audioCc = 0;
|
|
10403
|
+
framesSinceTableSend = 0;
|
|
10404
|
+
tablesSent = false;
|
|
10222
10405
|
constructor(options) {
|
|
10223
|
-
this.
|
|
10406
|
+
this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
10407
|
+
this.includeAudio = options.includeAudio ?? true;
|
|
10224
10408
|
}
|
|
10225
10409
|
/**
|
|
10226
|
-
*
|
|
10410
|
+
* Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
|
|
10411
|
+
* PAT and PMT are emitted before keyframes and periodically.
|
|
10412
|
+
*
|
|
10413
|
+
* @param annexBData - Annex-B video data (with start codes)
|
|
10414
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
10415
|
+
* @param isKeyframe - Whether this is an IDR / IRAP frame
|
|
10227
10416
|
*/
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10417
|
+
muxVideo(annexBData, ptsUs, isKeyframe) {
|
|
10418
|
+
const chunks = [];
|
|
10419
|
+
const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
|
|
10420
|
+
if (needTables) {
|
|
10421
|
+
chunks.push(buildPat(this.patCc));
|
|
10422
|
+
this.patCc = this.patCc + 1 & 15;
|
|
10423
|
+
chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
|
|
10424
|
+
this.pmtCc = this.pmtCc + 1 & 15;
|
|
10425
|
+
this.tablesSent = true;
|
|
10426
|
+
this.framesSinceTableSend = 0;
|
|
10427
|
+
}
|
|
10428
|
+
this.framesSinceTableSend++;
|
|
10429
|
+
const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
|
|
10430
|
+
const ccRef = { cc: this.videoCc };
|
|
10431
|
+
chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
|
|
10432
|
+
this.videoCc = ccRef.cc;
|
|
10433
|
+
return Buffer.concat(chunks);
|
|
10232
10434
|
}
|
|
10233
10435
|
/**
|
|
10234
|
-
* Mux
|
|
10436
|
+
* Mux an audio frame (ADTS AAC) into MPEG-TS packets.
|
|
10437
|
+
* Returns an empty Buffer when includeAudio is false.
|
|
10235
10438
|
*
|
|
10236
|
-
* @param
|
|
10237
|
-
* @param
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
const
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
this.
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10439
|
+
* @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
|
|
10440
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
10441
|
+
*/
|
|
10442
|
+
muxAudio(adtsData, ptsUs) {
|
|
10443
|
+
if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
|
|
10444
|
+
const pes = buildAudioPes(adtsData, ptsUs);
|
|
10445
|
+
const ccRef = { cc: this.audioCc };
|
|
10446
|
+
const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
|
|
10447
|
+
this.audioCc = ccRef.cc;
|
|
10448
|
+
return result;
|
|
10449
|
+
}
|
|
10450
|
+
/** Reset all continuity counters and table state (e.g. after stream restart). */
|
|
10451
|
+
reset() {
|
|
10452
|
+
this.patCc = 0;
|
|
10453
|
+
this.pmtCc = 0;
|
|
10454
|
+
this.videoCc = 0;
|
|
10455
|
+
this.audioCc = 0;
|
|
10456
|
+
this.framesSinceTableSend = 0;
|
|
10457
|
+
this.tablesSent = false;
|
|
10254
10458
|
}
|
|
10255
10459
|
};
|
|
10256
10460
|
|
|
@@ -15526,6 +15730,53 @@ var getAiStateViaGetAiAlarm = async (params) => {
|
|
|
15526
15730
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "getAiState failed"));
|
|
15527
15731
|
};
|
|
15528
15732
|
|
|
15733
|
+
// src/reolink/baichuan/utils/sleepInference.ts
|
|
15734
|
+
function decideSleepInferenceTransition(input) {
|
|
15735
|
+
const { inferred, committed, pending, hysteresisPolls } = input;
|
|
15736
|
+
if (committed === void 0) {
|
|
15737
|
+
return {
|
|
15738
|
+
emit: inferred === "sleeping" ? "sleeping" : null,
|
|
15739
|
+
nextCommitted: inferred,
|
|
15740
|
+
nextPending: void 0
|
|
15741
|
+
};
|
|
15742
|
+
}
|
|
15743
|
+
if (inferred === committed) {
|
|
15744
|
+
return {
|
|
15745
|
+
emit: null,
|
|
15746
|
+
nextCommitted: committed,
|
|
15747
|
+
nextPending: void 0
|
|
15748
|
+
};
|
|
15749
|
+
}
|
|
15750
|
+
const effectivePolls = Math.max(1, hysteresisPolls);
|
|
15751
|
+
if (!pending || pending.state !== inferred) {
|
|
15752
|
+
if (effectivePolls <= 1) {
|
|
15753
|
+
return {
|
|
15754
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
15755
|
+
nextCommitted: inferred,
|
|
15756
|
+
nextPending: void 0
|
|
15757
|
+
};
|
|
15758
|
+
}
|
|
15759
|
+
return {
|
|
15760
|
+
emit: null,
|
|
15761
|
+
nextCommitted: committed,
|
|
15762
|
+
nextPending: { state: inferred, count: 1 }
|
|
15763
|
+
};
|
|
15764
|
+
}
|
|
15765
|
+
const nextCount = pending.count + 1;
|
|
15766
|
+
if (nextCount < effectivePolls) {
|
|
15767
|
+
return {
|
|
15768
|
+
emit: null,
|
|
15769
|
+
nextCommitted: committed,
|
|
15770
|
+
nextPending: { state: inferred, count: nextCount }
|
|
15771
|
+
};
|
|
15772
|
+
}
|
|
15773
|
+
return {
|
|
15774
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
15775
|
+
nextCommitted: inferred,
|
|
15776
|
+
nextPending: void 0
|
|
15777
|
+
};
|
|
15778
|
+
}
|
|
15779
|
+
|
|
15529
15780
|
// src/reolink/baichuan/utils/channelInfoPush.ts
|
|
15530
15781
|
init_xml();
|
|
15531
15782
|
var parseOptionalInt = (value) => {
|
|
@@ -17495,6 +17746,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17495
17746
|
* - "replay:XXX" - dedicated per replay session
|
|
17496
17747
|
*/
|
|
17497
17748
|
socketPool = /* @__PURE__ */ new Map();
|
|
17749
|
+
/**
|
|
17750
|
+
* Consecutive stream-start (cmdId=3) timeout counter per socket tag.
|
|
17751
|
+
* When a streaming socket has N consecutive timeouts, the socket is force-closed
|
|
17752
|
+
* so the next attempt creates a fresh connection. Resets on success.
|
|
17753
|
+
*/
|
|
17754
|
+
consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
|
|
17755
|
+
static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
|
|
17498
17756
|
/** BaichuanClientOptions to use when creating new sockets */
|
|
17499
17757
|
clientOptions;
|
|
17500
17758
|
/**
|
|
@@ -17649,14 +17907,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17649
17907
|
if (!xml) return;
|
|
17650
17908
|
const channel = frame.header.channelId;
|
|
17651
17909
|
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
17652
|
-
if (battery.batteryPercent
|
|
17653
|
-
|
|
17654
|
-
type: "battery",
|
|
17655
|
-
channel,
|
|
17656
|
-
timestamp: Date.now(),
|
|
17657
|
-
battery
|
|
17658
|
-
});
|
|
17910
|
+
if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
|
|
17911
|
+
return;
|
|
17659
17912
|
}
|
|
17913
|
+
const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
|
|
17914
|
+
if (this.lastBatteryPushKey.get(channel) === key) {
|
|
17915
|
+
return;
|
|
17916
|
+
}
|
|
17917
|
+
this.lastBatteryPushKey.set(channel, key);
|
|
17918
|
+
this.dispatchSimpleEvent({
|
|
17919
|
+
type: "battery",
|
|
17920
|
+
channel,
|
|
17921
|
+
timestamp: Date.now(),
|
|
17922
|
+
battery
|
|
17923
|
+
});
|
|
17660
17924
|
} catch (e) {
|
|
17661
17925
|
this.logger.debug?.(
|
|
17662
17926
|
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
@@ -17789,7 +18053,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17789
18053
|
statePollingInterval;
|
|
17790
18054
|
udpSleepInferenceInterval;
|
|
17791
18055
|
udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
18056
|
+
/**
|
|
18057
|
+
* Per-channel pending sleep-state candidate for hysteresis.
|
|
18058
|
+
* When the inference flips to a new state we require N consecutive polls
|
|
18059
|
+
* of that same state before committing it — this filters out transient
|
|
18060
|
+
* flapping caused by non-waking traffic drifting in/out of the 10 s
|
|
18061
|
+
* getSleepStatus() observation window during stream teardown.
|
|
18062
|
+
*/
|
|
18063
|
+
udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
17792
18064
|
udpSleepInferenceIntervalMs = 2e3;
|
|
18065
|
+
/** Consecutive inference polls required to commit a new sleeping/awake state. */
|
|
18066
|
+
udpSleepInferenceHysteresisPolls = 2;
|
|
17793
18067
|
lastMotionState;
|
|
17794
18068
|
lastAiState;
|
|
17795
18069
|
aiStatePollingDisabled = false;
|
|
@@ -17822,6 +18096,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17822
18096
|
deviceCapabilitiesCache = /* @__PURE__ */ new Map();
|
|
17823
18097
|
static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
17824
18098
|
// 5 minutes
|
|
18099
|
+
/**
|
|
18100
|
+
* Dedupe key for battery push events (cmd_id 252), per channel.
|
|
18101
|
+
* Cameras emit BatteryInfoList frequently while streaming (every few
|
|
18102
|
+
* seconds). We only forward an event when the meaningful fields change
|
|
18103
|
+
* (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
|
|
18104
|
+
* consumers and the UI event log.
|
|
18105
|
+
*/
|
|
18106
|
+
lastBatteryPushKey = /* @__PURE__ */ new Map();
|
|
17825
18107
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
17826
18108
|
// SOCKET POOL CONSTANTS
|
|
17827
18109
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -18248,6 +18530,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18248
18530
|
*/
|
|
18249
18531
|
attachD2cDiscListener(client) {
|
|
18250
18532
|
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
18533
|
+
client.on("error", () => {
|
|
18534
|
+
});
|
|
18251
18535
|
}
|
|
18252
18536
|
/**
|
|
18253
18537
|
* Acquire a socket from the pool by tag.
|
|
@@ -18366,6 +18650,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18366
18650
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18367
18651
|
const newClient = new BaichuanClient(clientOpts);
|
|
18368
18652
|
this.attachD2cDiscListener(newClient);
|
|
18653
|
+
newClient.on("error", (err) => {
|
|
18654
|
+
log?.debug?.(
|
|
18655
|
+
`[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
|
|
18656
|
+
);
|
|
18657
|
+
});
|
|
18369
18658
|
await newClient.login();
|
|
18370
18659
|
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18371
18660
|
if (existingCooldown) {
|
|
@@ -18881,6 +19170,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18881
19170
|
* Only counts sessions from our own IP address.
|
|
18882
19171
|
*/
|
|
18883
19172
|
async maybeRebootOnTooManySessions() {
|
|
19173
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
18884
19174
|
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
18885
19175
|
if (this.sessionGuardRebootInFlight) return;
|
|
18886
19176
|
const cooldownMs = 10 * 6e4;
|
|
@@ -19322,6 +19612,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19322
19612
|
}
|
|
19323
19613
|
async renewSimpleEventSubscription() {
|
|
19324
19614
|
if (this.simpleEventListeners.size === 0) return;
|
|
19615
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
19325
19616
|
if (this.simpleEventResubscribeInFlight)
|
|
19326
19617
|
return await this.simpleEventResubscribeInFlight;
|
|
19327
19618
|
this.simpleEventResubscribeInFlight = (async () => {
|
|
@@ -23089,23 +23380,32 @@ ${stderr}`)
|
|
|
23089
23380
|
return;
|
|
23090
23381
|
}
|
|
23091
23382
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
23383
|
+
if (!this.client.isSocketConnected?.()) {
|
|
23384
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23385
|
+
return;
|
|
23386
|
+
}
|
|
23092
23387
|
const status = this.getSleepStatus({ channel });
|
|
23093
23388
|
if (status.state === "unknown") return;
|
|
23094
|
-
const
|
|
23095
|
-
this.
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23389
|
+
const committed = this.udpLastInferredSleepStateByChannel.get(channel);
|
|
23390
|
+
const pending = this.udpPendingSleepStateByChannel.get(channel);
|
|
23391
|
+
const decision = decideSleepInferenceTransition({
|
|
23392
|
+
inferred: status.state,
|
|
23393
|
+
committed,
|
|
23394
|
+
pending,
|
|
23395
|
+
hysteresisPolls: this.udpSleepInferenceHysteresisPolls
|
|
23396
|
+
});
|
|
23397
|
+
this.udpLastInferredSleepStateByChannel.set(
|
|
23398
|
+
channel,
|
|
23399
|
+
decision.nextCommitted
|
|
23400
|
+
);
|
|
23401
|
+
if (decision.nextPending === void 0) {
|
|
23402
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23403
|
+
} else {
|
|
23404
|
+
this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
|
|
23105
23405
|
}
|
|
23106
|
-
if (
|
|
23406
|
+
if (decision.emit) {
|
|
23107
23407
|
this.dispatchSimpleEvent({
|
|
23108
|
-
type:
|
|
23408
|
+
type: decision.emit,
|
|
23109
23409
|
channel,
|
|
23110
23410
|
timestamp: Date.now()
|
|
23111
23411
|
});
|
|
@@ -23128,6 +23428,7 @@ ${stderr}`)
|
|
|
23128
23428
|
this.udpSleepInferenceInterval = void 0;
|
|
23129
23429
|
}
|
|
23130
23430
|
this.udpLastInferredSleepStateByChannel.clear();
|
|
23431
|
+
this.udpPendingSleepStateByChannel.clear();
|
|
23131
23432
|
}
|
|
23132
23433
|
/**
|
|
23133
23434
|
* GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
|
|
@@ -23294,6 +23595,7 @@ ${stderr}`)
|
|
|
23294
23595
|
`${ch}:${profile}:${variant}`,
|
|
23295
23596
|
frame.header.msgNum
|
|
23296
23597
|
);
|
|
23598
|
+
this.resetStreamTimeoutCounter(targetClient);
|
|
23297
23599
|
return;
|
|
23298
23600
|
} catch (error) {
|
|
23299
23601
|
lastError = error;
|
|
@@ -23308,6 +23610,10 @@ ${stderr}`)
|
|
|
23308
23610
|
}
|
|
23309
23611
|
}
|
|
23310
23612
|
}
|
|
23613
|
+
const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
|
|
23614
|
+
if (isTimeout) {
|
|
23615
|
+
this.trackStreamTimeout(targetClient);
|
|
23616
|
+
}
|
|
23311
23617
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
23312
23618
|
}
|
|
23313
23619
|
/**
|
|
@@ -23767,6 +24073,18 @@ ${stderr}`)
|
|
|
23767
24073
|
notifyD2cDisc() {
|
|
23768
24074
|
const now = Date.now();
|
|
23769
24075
|
this.lastD2cDiscAtMs = now;
|
|
24076
|
+
const streamingTags = Array.from(this.socketPool.keys()).filter(
|
|
24077
|
+
(tag) => tag.startsWith("streaming:")
|
|
24078
|
+
);
|
|
24079
|
+
if (streamingTags.length > 0) {
|
|
24080
|
+
this.logger?.log?.(
|
|
24081
|
+
`[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
|
|
24082
|
+
);
|
|
24083
|
+
for (const tag of streamingTags) {
|
|
24084
|
+
this.forceClosePooledSocket(tag, this.logger).catch(() => {
|
|
24085
|
+
});
|
|
24086
|
+
}
|
|
24087
|
+
}
|
|
23770
24088
|
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
23771
24089
|
const existing = this.socketPoolCooldowns.get(this.host);
|
|
23772
24090
|
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
@@ -23799,6 +24117,43 @@ ${stderr}`)
|
|
|
23799
24117
|
}
|
|
23800
24118
|
}
|
|
23801
24119
|
}
|
|
24120
|
+
/**
|
|
24121
|
+
* Find the socket pool tag for a given BaichuanClient instance.
|
|
24122
|
+
* Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
|
|
24123
|
+
*/
|
|
24124
|
+
findSocketTagForClient(client) {
|
|
24125
|
+
for (const [tag, entry] of this.socketPool) {
|
|
24126
|
+
if (entry.client === client) return tag;
|
|
24127
|
+
}
|
|
24128
|
+
return void 0;
|
|
24129
|
+
}
|
|
24130
|
+
/**
|
|
24131
|
+
* Reset the consecutive stream-start timeout counter for a streaming socket.
|
|
24132
|
+
* Called on successful stream start.
|
|
24133
|
+
*/
|
|
24134
|
+
resetStreamTimeoutCounter(client) {
|
|
24135
|
+
const tag = this.findSocketTagForClient(client);
|
|
24136
|
+
if (tag) this.consecutiveStreamTimeouts.delete(tag);
|
|
24137
|
+
}
|
|
24138
|
+
/**
|
|
24139
|
+
* Track a stream-start timeout on a streaming socket.
|
|
24140
|
+
* After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
|
|
24141
|
+
* socket so the next attempt creates a fresh connection.
|
|
24142
|
+
*/
|
|
24143
|
+
trackStreamTimeout(client) {
|
|
24144
|
+
const tag = this.findSocketTagForClient(client);
|
|
24145
|
+
if (!tag || !tag.startsWith("streaming:")) return;
|
|
24146
|
+
const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
|
|
24147
|
+
this.consecutiveStreamTimeouts.set(tag, count);
|
|
24148
|
+
if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
|
|
24149
|
+
this.logger?.warn?.(
|
|
24150
|
+
`[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
|
|
24151
|
+
);
|
|
24152
|
+
this.consecutiveStreamTimeouts.delete(tag);
|
|
24153
|
+
this.forceClosePooledSocket(tag, this.logger).catch(() => {
|
|
24154
|
+
});
|
|
24155
|
+
}
|
|
24156
|
+
}
|
|
23802
24157
|
/**
|
|
23803
24158
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
23804
24159
|
*
|
|
@@ -26672,8 +27027,8 @@ ${scheduleItems}
|
|
|
26672
27027
|
);
|
|
26673
27028
|
let args;
|
|
26674
27029
|
if (useMpegTsMuxer) {
|
|
26675
|
-
MpegTsMuxer
|
|
26676
|
-
tsMuxer
|
|
27030
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
27031
|
+
tsMuxer.reset();
|
|
26677
27032
|
args = [
|
|
26678
27033
|
"-hide_banner",
|
|
26679
27034
|
"-loglevel",
|
|
@@ -26837,7 +27192,7 @@ ${scheduleItems}
|
|
|
26837
27192
|
startFfmpeg(videoType);
|
|
26838
27193
|
frameCount++;
|
|
26839
27194
|
if (useMpegTsMuxer && tsMuxer) {
|
|
26840
|
-
const tsData = tsMuxer.
|
|
27195
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
26841
27196
|
input.write(tsData);
|
|
26842
27197
|
} else {
|
|
26843
27198
|
if (videoType === "H264") input.write(H264_AUD);
|
|
@@ -27126,8 +27481,8 @@ ${scheduleItems}
|
|
|
27126
27481
|
logger?.log?.(
|
|
27127
27482
|
`[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
|
|
27128
27483
|
);
|
|
27129
|
-
MpegTsMuxer
|
|
27130
|
-
tsMuxer
|
|
27484
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
27485
|
+
tsMuxer.reset();
|
|
27131
27486
|
const args = [
|
|
27132
27487
|
"-hide_banner",
|
|
27133
27488
|
"-loglevel",
|
|
@@ -27292,7 +27647,7 @@ ${scheduleItems}
|
|
|
27292
27647
|
startFfmpeg(videoType);
|
|
27293
27648
|
frameCount++;
|
|
27294
27649
|
if (tsMuxer) {
|
|
27295
|
-
const tsData = tsMuxer.
|
|
27650
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
27296
27651
|
input.write(tsData);
|
|
27297
27652
|
}
|
|
27298
27653
|
if (frameCount === 1) {
|
|
@@ -28036,16 +28391,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
|
|
|
28036
28391
|
return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("socket hang up") || message.includes("TCP connection timeout") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
|
|
28037
28392
|
}
|
|
28038
28393
|
async function pingHost(host, timeoutMs = 3e3) {
|
|
28394
|
+
const { exec } = await import("child_process");
|
|
28395
|
+
const platform2 = process.platform;
|
|
28396
|
+
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
28397
|
+
// macOS: -W is in milliseconds (Linux: seconds)
|
|
28398
|
+
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
28399
|
+
) : (
|
|
28400
|
+
// Linux/BSD-ish: -W is in seconds on most distros
|
|
28401
|
+
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
28402
|
+
);
|
|
28039
28403
|
return new Promise((resolve) => {
|
|
28040
|
-
const { exec } = require("child_process");
|
|
28041
|
-
const platform2 = process.platform;
|
|
28042
|
-
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
28043
|
-
// macOS: -W is in milliseconds (Linux: seconds)
|
|
28044
|
-
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
28045
|
-
) : (
|
|
28046
|
-
// Linux/BSD-ish: -W is in seconds on most distros
|
|
28047
|
-
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
28048
|
-
);
|
|
28049
28404
|
exec(pingCmd, (error) => {
|
|
28050
28405
|
resolve(!error);
|
|
28051
28406
|
});
|
|
@@ -28127,9 +28482,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28127
28482
|
const msg = fmtErr(e);
|
|
28128
28483
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
28129
28484
|
};
|
|
28130
|
-
const withRetries = async (label, max, op, shouldRetry) => {
|
|
28485
|
+
const withRetries = async (label, max, op, shouldRetry, isAborted) => {
|
|
28131
28486
|
let lastErr;
|
|
28132
28487
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
28488
|
+
if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
|
|
28133
28489
|
try {
|
|
28134
28490
|
if (attempt > 1) {
|
|
28135
28491
|
logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
|
|
@@ -28138,7 +28494,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28138
28494
|
} catch (e) {
|
|
28139
28495
|
lastErr = e;
|
|
28140
28496
|
const msg = fmtErr(e);
|
|
28141
|
-
const retryable = attempt < max && shouldRetry(e);
|
|
28497
|
+
const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
|
|
28142
28498
|
logger?.log?.(
|
|
28143
28499
|
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
28144
28500
|
);
|
|
@@ -28148,6 +28504,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28148
28504
|
}
|
|
28149
28505
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
|
|
28150
28506
|
};
|
|
28507
|
+
const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
|
|
28508
|
+
let raceWon = false;
|
|
28509
|
+
const methodErrors = /* @__PURE__ */ new Map();
|
|
28510
|
+
try {
|
|
28511
|
+
return await Promise.any(
|
|
28512
|
+
methods.map(async (m) => {
|
|
28513
|
+
try {
|
|
28514
|
+
const result = await loginAndDetect(m, () => raceWon);
|
|
28515
|
+
raceWon = true;
|
|
28516
|
+
return result;
|
|
28517
|
+
} catch (e) {
|
|
28518
|
+
if (!raceWon) methodErrors.set(m, fmtErr(e));
|
|
28519
|
+
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
|
|
28520
|
+
throw e;
|
|
28521
|
+
}
|
|
28522
|
+
})
|
|
28523
|
+
);
|
|
28524
|
+
} catch (e) {
|
|
28525
|
+
if (e instanceof AggregateError) {
|
|
28526
|
+
const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
|
|
28527
|
+
throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
|
|
28528
|
+
}
|
|
28529
|
+
throw e;
|
|
28530
|
+
}
|
|
28531
|
+
};
|
|
28151
28532
|
const effectiveUid = normalizeUid(uid);
|
|
28152
28533
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
28153
28534
|
const isReachable = await pingHost(host);
|
|
@@ -28177,9 +28558,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28177
28558
|
normalizedUid = normalizedDiscovered;
|
|
28178
28559
|
}
|
|
28179
28560
|
const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
28180
|
-
|
|
28181
|
-
|
|
28182
|
-
|
|
28561
|
+
return await runUdpMethodsParallel(
|
|
28562
|
+
methodsToTry,
|
|
28563
|
+
async (m, isAborted) => {
|
|
28183
28564
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
28184
28565
|
const udpApi = await withRetries(
|
|
28185
28566
|
`UDP(${m})`,
|
|
@@ -28202,11 +28583,14 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28202
28583
|
throw e;
|
|
28203
28584
|
}
|
|
28204
28585
|
},
|
|
28205
|
-
shouldRetryUdp
|
|
28586
|
+
shouldRetryUdp,
|
|
28587
|
+
isAborted
|
|
28206
28588
|
);
|
|
28207
|
-
const deviceInfo = await
|
|
28208
|
-
|
|
28209
|
-
|
|
28589
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
28590
|
+
udpApi.getInfo(),
|
|
28591
|
+
udpApi.getDeviceCapabilities(),
|
|
28592
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
28593
|
+
]);
|
|
28210
28594
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
28211
28595
|
const model = deviceInfo.type?.trim();
|
|
28212
28596
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -28245,14 +28629,8 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28245
28629
|
channelNum: 1,
|
|
28246
28630
|
api: udpApi
|
|
28247
28631
|
};
|
|
28248
|
-
}
|
|
28249
|
-
|
|
28250
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
28251
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
28252
|
-
}
|
|
28253
|
-
}
|
|
28254
|
-
throw new Error(
|
|
28255
|
-
`Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
|
|
28632
|
+
},
|
|
28633
|
+
"Forced UDP autodetect failed for all methods."
|
|
28256
28634
|
);
|
|
28257
28635
|
}
|
|
28258
28636
|
let tcpApi;
|
|
@@ -28305,54 +28683,57 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28305
28683
|
}
|
|
28306
28684
|
return void 0;
|
|
28307
28685
|
};
|
|
28308
|
-
const infoProbe = await
|
|
28309
|
-
|
|
28310
|
-
|
|
28686
|
+
const [infoProbe, supportProbe] = await Promise.all([
|
|
28687
|
+
runProbeVariants(
|
|
28688
|
+
"getInfo",
|
|
28689
|
+
[
|
|
28690
|
+
{
|
|
28691
|
+
variant: "cmd80 class=0x6414",
|
|
28692
|
+
op: () => api.getInfo(void 0, {
|
|
28693
|
+
timeoutMs: 2500,
|
|
28694
|
+
messageClass: BC_CLASS_MODERN_24
|
|
28695
|
+
})
|
|
28696
|
+
},
|
|
28697
|
+
{
|
|
28698
|
+
variant: "cmd80 class=0x6614",
|
|
28699
|
+
op: () => api.getInfo(void 0, {
|
|
28700
|
+
timeoutMs: 3e3,
|
|
28701
|
+
messageClass: BC_CLASS_MODERN_20
|
|
28702
|
+
})
|
|
28703
|
+
},
|
|
28704
|
+
{
|
|
28705
|
+
variant: "cmd318(ch0) class=0x6414",
|
|
28706
|
+
op: () => api.getInfo(0, {
|
|
28707
|
+
timeoutMs: 3e3,
|
|
28708
|
+
messageClass: BC_CLASS_MODERN_24
|
|
28709
|
+
})
|
|
28710
|
+
},
|
|
28711
|
+
{
|
|
28712
|
+
variant: "cmd318(ch0) class=0x6614",
|
|
28713
|
+
op: () => api.getInfo(0, {
|
|
28714
|
+
timeoutMs: 3500,
|
|
28715
|
+
messageClass: BC_CLASS_MODERN_20
|
|
28716
|
+
})
|
|
28717
|
+
}
|
|
28718
|
+
]
|
|
28719
|
+
),
|
|
28720
|
+
// Support probes (cmd 199). Some firmwares may not support it or are slow.
|
|
28721
|
+
runProbeVariants("getSupportInfo", [
|
|
28311
28722
|
{
|
|
28312
|
-
variant: "
|
|
28313
|
-
op: () => api.
|
|
28723
|
+
variant: "cmd199 class=0x6414",
|
|
28724
|
+
op: () => api.getSupportInfo({
|
|
28314
28725
|
timeoutMs: 2500,
|
|
28315
28726
|
messageClass: BC_CLASS_MODERN_24
|
|
28316
28727
|
})
|
|
28317
28728
|
},
|
|
28318
28729
|
{
|
|
28319
|
-
variant: "
|
|
28320
|
-
op: () => api.
|
|
28321
|
-
timeoutMs: 3e3,
|
|
28322
|
-
messageClass: BC_CLASS_MODERN_20
|
|
28323
|
-
})
|
|
28324
|
-
},
|
|
28325
|
-
{
|
|
28326
|
-
variant: "cmd318(ch0) class=0x6414",
|
|
28327
|
-
op: () => api.getInfo(0, {
|
|
28328
|
-
timeoutMs: 3e3,
|
|
28329
|
-
messageClass: BC_CLASS_MODERN_24
|
|
28330
|
-
})
|
|
28331
|
-
},
|
|
28332
|
-
{
|
|
28333
|
-
variant: "cmd318(ch0) class=0x6614",
|
|
28334
|
-
op: () => api.getInfo(0, {
|
|
28730
|
+
variant: "cmd199 class=0x6614",
|
|
28731
|
+
op: () => api.getSupportInfo({
|
|
28335
28732
|
timeoutMs: 3500,
|
|
28336
28733
|
messageClass: BC_CLASS_MODERN_20
|
|
28337
28734
|
})
|
|
28338
28735
|
}
|
|
28339
|
-
]
|
|
28340
|
-
);
|
|
28341
|
-
const supportProbe = await runProbeVariants("getSupportInfo", [
|
|
28342
|
-
{
|
|
28343
|
-
variant: "cmd199 class=0x6414",
|
|
28344
|
-
op: () => api.getSupportInfo({
|
|
28345
|
-
timeoutMs: 2500,
|
|
28346
|
-
messageClass: BC_CLASS_MODERN_24
|
|
28347
|
-
})
|
|
28348
|
-
},
|
|
28349
|
-
{
|
|
28350
|
-
variant: "cmd199 class=0x6614",
|
|
28351
|
-
op: () => api.getSupportInfo({
|
|
28352
|
-
timeoutMs: 3500,
|
|
28353
|
-
messageClass: BC_CLASS_MODERN_20
|
|
28354
|
-
})
|
|
28355
|
-
}
|
|
28736
|
+
])
|
|
28356
28737
|
]);
|
|
28357
28738
|
const deviceInfo = infoProbe?.value;
|
|
28358
28739
|
const support = supportProbe?.value;
|
|
@@ -28444,9 +28825,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28444
28825
|
}
|
|
28445
28826
|
try {
|
|
28446
28827
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
28447
|
-
const deviceInfo = await
|
|
28448
|
-
|
|
28449
|
-
|
|
28828
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
28829
|
+
udpApi.getInfo(),
|
|
28830
|
+
udpApi.getDeviceCapabilities(),
|
|
28831
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
28832
|
+
]);
|
|
28450
28833
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
28451
28834
|
const model = deviceInfo.type?.trim();
|
|
28452
28835
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -28490,21 +28873,17 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28490
28873
|
};
|
|
28491
28874
|
};
|
|
28492
28875
|
const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
28493
|
-
const
|
|
28494
|
-
|
|
28495
|
-
|
|
28876
|
+
const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
|
|
28877
|
+
return await runUdpMethodsParallel(
|
|
28878
|
+
viableMethods,
|
|
28879
|
+
async (m, isAborted) => {
|
|
28496
28880
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
28497
28881
|
const udpApi = await withRetries(
|
|
28498
28882
|
`UDP(${m})`,
|
|
28499
28883
|
maxRetries,
|
|
28500
28884
|
async (attempt) => {
|
|
28501
|
-
const apiInputs = {
|
|
28502
|
-
|
|
28503
|
-
udpDiscoveryMethod: m
|
|
28504
|
-
};
|
|
28505
|
-
if (normalizedUid) {
|
|
28506
|
-
apiInputs.uid = normalizedUid;
|
|
28507
|
-
}
|
|
28885
|
+
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
28886
|
+
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
28508
28887
|
const api = createBaichuanApi(apiInputs, "udp");
|
|
28509
28888
|
try {
|
|
28510
28889
|
await api.login();
|
|
@@ -28519,20 +28898,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28519
28898
|
throw e;
|
|
28520
28899
|
}
|
|
28521
28900
|
},
|
|
28522
|
-
shouldRetryUdp
|
|
28901
|
+
shouldRetryUdp,
|
|
28902
|
+
isAborted
|
|
28523
28903
|
);
|
|
28524
|
-
return
|
|
28525
|
-
}
|
|
28526
|
-
|
|
28527
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
28528
|
-
try {
|
|
28529
|
-
} catch {
|
|
28530
|
-
}
|
|
28531
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
28532
|
-
}
|
|
28533
|
-
}
|
|
28534
|
-
throw new Error(
|
|
28535
|
-
`UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
|
|
28904
|
+
return detectOverUdpApi(udpApi, m);
|
|
28905
|
+
},
|
|
28906
|
+
"UDP discovery failed for all methods."
|
|
28536
28907
|
);
|
|
28537
28908
|
} catch (udpError) {
|
|
28538
28909
|
logger?.log?.(
|