@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/index.cjs
CHANGED
|
@@ -2462,7 +2462,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2462
2462
|
const allowMsgNum0Fallback = this.acceptAnyStreamType && frame.header.msgNum === 0;
|
|
2463
2463
|
if (!allowMsgNum0Fallback) {
|
|
2464
2464
|
const frameCount = this._msgNumMismatchCount = (this._msgNumMismatchCount || 0) + 1;
|
|
2465
|
-
if (frameCount <= 5) {
|
|
2465
|
+
if (frameCount <= 5 && this.client.getDebugConfig().general) {
|
|
2466
2466
|
this.logger?.log(
|
|
2467
2467
|
`[BaichuanVideoStream] Frame msgNum mismatch: received=${frame.header.msgNum}, expected=${this.activeMsgNum}, channel=${this.channel}, profile=${this.profile}, variant=${this.variant} (frame discarded)`
|
|
2468
2468
|
);
|
|
@@ -2472,7 +2472,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2472
2472
|
}
|
|
2473
2473
|
if (!this.acceptAnyStreamType && !this.expectedStreamTypes.has(frame.header.streamType)) {
|
|
2474
2474
|
const frameCount = this._streamTypeMismatchCount = (this._streamTypeMismatchCount || 0) + 1;
|
|
2475
|
-
if (frameCount <= 5) {
|
|
2475
|
+
if (frameCount <= 5 && this.client.getDebugConfig().general) {
|
|
2476
2476
|
this.logger?.log(
|
|
2477
2477
|
`[BaichuanVideoStream] Frame streamType mismatch: received=${frame.header.streamType}, expectedAny=[${[
|
|
2478
2478
|
...this.expectedStreamTypes
|
|
@@ -7896,6 +7896,7 @@ __export(index_exports, {
|
|
|
7896
7896
|
HlsSessionManager: () => HlsSessionManager,
|
|
7897
7897
|
Intercom: () => Intercom,
|
|
7898
7898
|
MjpegTransformer: () => MjpegTransformer,
|
|
7899
|
+
MpegTsMuxer: () => MpegTsMuxer,
|
|
7899
7900
|
NVR_HUB_EXACT_TYPES: () => NVR_HUB_EXACT_TYPES,
|
|
7900
7901
|
NVR_HUB_MODEL_PATTERNS: () => NVR_HUB_MODEL_PATTERNS,
|
|
7901
7902
|
ReolinkBaichuanApi: () => ReolinkBaichuanApi,
|
|
@@ -7954,6 +7955,7 @@ __export(index_exports, {
|
|
|
7954
7955
|
createRfc4571TcpServerForReplay: () => createRfc4571TcpServerForReplay,
|
|
7955
7956
|
createRtspProxyServer: () => createRtspProxyServer,
|
|
7956
7957
|
createTaggedLogger: () => createTaggedLogger,
|
|
7958
|
+
decideSleepInferenceTransition: () => decideSleepInferenceTransition,
|
|
7957
7959
|
decideVideoclipTranscodeMode: () => decideVideoclipTranscodeMode,
|
|
7958
7960
|
decodeHeader: () => decodeHeader,
|
|
7959
7961
|
deriveAesKey: () => deriveAesKey,
|
|
@@ -13234,19 +13236,34 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
13234
13236
|
}
|
|
13235
13237
|
});
|
|
13236
13238
|
streamStarted = true;
|
|
13237
|
-
|
|
13239
|
+
const signal = options?.signal;
|
|
13240
|
+
while (!closed && !signal?.aborted) {
|
|
13238
13241
|
if (frameQueue.length > 0) {
|
|
13239
13242
|
const frame = frameQueue.shift();
|
|
13240
13243
|
yield frame;
|
|
13241
13244
|
} else {
|
|
13242
13245
|
await new Promise((resolve) => {
|
|
13243
13246
|
frameResolve = resolve;
|
|
13244
|
-
setTimeout(() => {
|
|
13247
|
+
const timer = setTimeout(() => {
|
|
13245
13248
|
if (frameResolve === resolve) {
|
|
13246
13249
|
frameResolve = null;
|
|
13247
13250
|
resolve();
|
|
13248
13251
|
}
|
|
13249
13252
|
}, 1e3);
|
|
13253
|
+
if (signal) {
|
|
13254
|
+
const onAbort = () => {
|
|
13255
|
+
clearTimeout(timer);
|
|
13256
|
+
if (frameResolve === resolve) frameResolve = null;
|
|
13257
|
+
resolve();
|
|
13258
|
+
};
|
|
13259
|
+
if (signal.aborted) {
|
|
13260
|
+
clearTimeout(timer);
|
|
13261
|
+
frameResolve = null;
|
|
13262
|
+
resolve();
|
|
13263
|
+
} else {
|
|
13264
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
13265
|
+
}
|
|
13266
|
+
}
|
|
13250
13267
|
});
|
|
13251
13268
|
}
|
|
13252
13269
|
}
|
|
@@ -13419,13 +13436,14 @@ var NativeStreamFanout = class {
|
|
|
13419
13436
|
source = null;
|
|
13420
13437
|
running = false;
|
|
13421
13438
|
pumpPromise = null;
|
|
13439
|
+
abort = new AbortController();
|
|
13422
13440
|
constructor(opts) {
|
|
13423
13441
|
this.opts = opts;
|
|
13424
13442
|
}
|
|
13425
13443
|
start() {
|
|
13426
13444
|
if (this.running) return;
|
|
13427
13445
|
this.running = true;
|
|
13428
|
-
this.source = this.opts.createSource();
|
|
13446
|
+
this.source = this.opts.createSource(this.abort.signal);
|
|
13429
13447
|
this.pumpPromise = (async () => {
|
|
13430
13448
|
try {
|
|
13431
13449
|
for await (const frame of this.source) {
|
|
@@ -13471,6 +13489,7 @@ var NativeStreamFanout = class {
|
|
|
13471
13489
|
this.source = null;
|
|
13472
13490
|
for (const q of this.queues.values()) q.close();
|
|
13473
13491
|
this.queues.clear();
|
|
13492
|
+
this.abort.abort();
|
|
13474
13493
|
try {
|
|
13475
13494
|
await src?.return(void 0);
|
|
13476
13495
|
} catch {
|
|
@@ -13509,9 +13528,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13509
13528
|
requireAuth;
|
|
13510
13529
|
authNonces = /* @__PURE__ */ new Map();
|
|
13511
13530
|
// Track nonces per client
|
|
13512
|
-
AUTH_REALM
|
|
13531
|
+
AUTH_REALM;
|
|
13513
13532
|
NONCE_TIMEOUT_MS = 3e5;
|
|
13514
13533
|
// 5 minutes
|
|
13534
|
+
lazyMetadata;
|
|
13515
13535
|
// Client tracking
|
|
13516
13536
|
connectedClients = /* @__PURE__ */ new Set();
|
|
13517
13537
|
// Set of client IDs (IP:port)
|
|
@@ -13523,8 +13543,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13523
13543
|
// Track all client resources for cleanup
|
|
13524
13544
|
clientResources = /* @__PURE__ */ new Map();
|
|
13525
13545
|
isRtspDebugEnabled() {
|
|
13526
|
-
|
|
13527
|
-
|
|
13546
|
+
try {
|
|
13547
|
+
if (this.api.isClosed) {
|
|
13548
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
13549
|
+
}
|
|
13550
|
+
const dbg = this.api.client.getDebugConfig();
|
|
13551
|
+
return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
13552
|
+
} catch {
|
|
13553
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
13554
|
+
}
|
|
13528
13555
|
}
|
|
13529
13556
|
rtspDebugLog(message) {
|
|
13530
13557
|
if (!this.isRtspDebugEnabled()) return;
|
|
@@ -13546,10 +13573,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13546
13573
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
13547
13574
|
nativeFanout = null;
|
|
13548
13575
|
noClientAutoStopTimer;
|
|
13576
|
+
/** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
|
|
13577
|
+
noFrameDeadlineTimer;
|
|
13549
13578
|
/** After last RTSP client; 0 = never auto-stop native stream. */
|
|
13550
13579
|
nativeStreamIdleStopMs;
|
|
13551
13580
|
/** Primed-but-no-PLAY timeout; 0 = disabled. */
|
|
13552
13581
|
nativeStreamPrimeIdleStopMs;
|
|
13582
|
+
/**
|
|
13583
|
+
* Max time to wait for the first camera frame after stream start.
|
|
13584
|
+
* If no frames arrive within this window, the native stream is stopped
|
|
13585
|
+
* (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
|
|
13586
|
+
* firing and waking the camera when no real viewer is watching.
|
|
13587
|
+
* 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
|
|
13588
|
+
*/
|
|
13589
|
+
nativeStreamNoFrameDeadlineMs;
|
|
13553
13590
|
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
13554
13591
|
// When a new client connects while the stream is already running it does not need
|
|
13555
13592
|
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
@@ -13675,14 +13712,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13675
13712
|
this.logger = options.logger ?? console;
|
|
13676
13713
|
this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
|
|
13677
13714
|
this.deviceId = options.deviceId;
|
|
13678
|
-
this.externalListener = options.externalListener ?? false;
|
|
13715
|
+
this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
|
|
13679
13716
|
this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
|
|
13680
13717
|
this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
|
|
13681
|
-
this.
|
|
13718
|
+
this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
|
|
13719
|
+
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
13720
|
+
username: c.username,
|
|
13721
|
+
...c.password !== void 0 ? { password: c.password } : {},
|
|
13722
|
+
...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
|
|
13723
|
+
}));
|
|
13682
13724
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
13725
|
+
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
13726
|
+
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
13683
13727
|
const transport = this.api.client.getTransport();
|
|
13684
13728
|
this.flow = createRtspFlow(transport, "H264");
|
|
13685
13729
|
}
|
|
13730
|
+
/** Number of currently connected RTSP clients. */
|
|
13731
|
+
get clientCount() {
|
|
13732
|
+
return this.connectedClients.size;
|
|
13733
|
+
}
|
|
13686
13734
|
// --- Authentication helpers ---
|
|
13687
13735
|
/**
|
|
13688
13736
|
* Generate a new nonce for Digest authentication
|
|
@@ -13743,9 +13791,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13743
13791
|
this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
|
|
13744
13792
|
return false;
|
|
13745
13793
|
}
|
|
13794
|
+
if (realm !== this.AUTH_REALM) {
|
|
13795
|
+
this.rtspDebugLog(
|
|
13796
|
+
`Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
|
|
13797
|
+
);
|
|
13798
|
+
return false;
|
|
13799
|
+
}
|
|
13746
13800
|
for (const cred of this.authCredentials) {
|
|
13747
13801
|
if (username !== cred.username) continue;
|
|
13748
|
-
const ha1 = this.md5(`${cred.username}:${
|
|
13802
|
+
const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
|
|
13803
|
+
if (!ha1) continue;
|
|
13749
13804
|
const ha2 = this.md5(`${method}:${authUri || uri}`);
|
|
13750
13805
|
const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
|
|
13751
13806
|
if (response === expectedResponse) {
|
|
@@ -13774,6 +13829,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13774
13829
|
this.noClientAutoStopTimer = void 0;
|
|
13775
13830
|
}
|
|
13776
13831
|
}
|
|
13832
|
+
clearNoFrameDeadlineTimer() {
|
|
13833
|
+
if (this.noFrameDeadlineTimer) {
|
|
13834
|
+
clearTimeout(this.noFrameDeadlineTimer);
|
|
13835
|
+
this.noFrameDeadlineTimer = void 0;
|
|
13836
|
+
}
|
|
13837
|
+
}
|
|
13777
13838
|
setFlowVideoType(videoType, reason) {
|
|
13778
13839
|
if (this.flow.videoType === videoType) return;
|
|
13779
13840
|
const transport = this.api.client.getTransport();
|
|
@@ -13788,25 +13849,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13788
13849
|
if (this.active) {
|
|
13789
13850
|
throw new Error("RTSP server is already active");
|
|
13790
13851
|
}
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
if (stream) {
|
|
13795
|
-
this.streamMetadata = {
|
|
13796
|
-
frameRate: stream.frameRate || 25,
|
|
13797
|
-
width: stream.width,
|
|
13798
|
-
height: stream.height
|
|
13799
|
-
};
|
|
13800
|
-
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
13801
|
-
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
13802
|
-
this.setFlowVideoType(metaVideoType, "metadata");
|
|
13803
|
-
}
|
|
13804
|
-
} catch (error) {
|
|
13805
|
-
this.logger.warn(
|
|
13806
|
-
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
13852
|
+
if (this.lazyMetadata) {
|
|
13853
|
+
this.logger.info(
|
|
13854
|
+
`[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
|
|
13807
13855
|
);
|
|
13808
|
-
|
|
13809
|
-
|
|
13856
|
+
} else {
|
|
13857
|
+
try {
|
|
13858
|
+
const metadata = await this.api.getStreamMetadata(this.channel);
|
|
13859
|
+
const stream = metadata.streams.find((s) => s.profile === this.profile);
|
|
13860
|
+
if (stream) {
|
|
13861
|
+
this.streamMetadata = {
|
|
13862
|
+
frameRate: stream.frameRate || 25,
|
|
13863
|
+
width: stream.width,
|
|
13864
|
+
height: stream.height
|
|
13865
|
+
};
|
|
13866
|
+
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
13867
|
+
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
13868
|
+
this.setFlowVideoType(metaVideoType, "metadata");
|
|
13869
|
+
}
|
|
13870
|
+
} catch (error) {
|
|
13871
|
+
this.logger.warn(
|
|
13872
|
+
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
13873
|
+
);
|
|
13874
|
+
this.streamMetadata = { frameRate: 25 };
|
|
13875
|
+
this.setFlowVideoType("H264", "metadata unavailable");
|
|
13876
|
+
}
|
|
13810
13877
|
}
|
|
13811
13878
|
if (!this.externalListener) {
|
|
13812
13879
|
this.clientConnectionServer = net2.createServer((socket) => {
|
|
@@ -13847,6 +13914,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13847
13914
|
}
|
|
13848
13915
|
this.handleRtspConnection(socket, initialBuffer);
|
|
13849
13916
|
}
|
|
13917
|
+
/**
|
|
13918
|
+
* Inject an already-accepted client socket from a multiplexer
|
|
13919
|
+
* (e.g. `LocalRtspMux`) that owns the listening port.
|
|
13920
|
+
*
|
|
13921
|
+
* The mux reads the first RTSP request line to determine the target path,
|
|
13922
|
+
* then hands the socket over. Any bytes already consumed during routing
|
|
13923
|
+
* are replayed back onto the socket via `unshift()` so the RTSP parser in
|
|
13924
|
+
* `handleRtspConnection` sees the complete original request.
|
|
13925
|
+
*
|
|
13926
|
+
* @param socket - Client TCP socket, already accepted by the mux.
|
|
13927
|
+
* @param preReadData - Bytes the mux has already pulled off the socket
|
|
13928
|
+
* while parsing the request line. Replayed via `socket.unshift()`
|
|
13929
|
+
* before any further reads.
|
|
13930
|
+
*/
|
|
13931
|
+
injectSocket(socket, preReadData) {
|
|
13932
|
+
if (!this.active) {
|
|
13933
|
+
socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
|
|
13934
|
+
return;
|
|
13935
|
+
}
|
|
13936
|
+
if (preReadData && preReadData.length > 0) {
|
|
13937
|
+
socket.unshift(preReadData);
|
|
13938
|
+
}
|
|
13939
|
+
this.handleRtspConnection(socket);
|
|
13940
|
+
}
|
|
13850
13941
|
/**
|
|
13851
13942
|
* Handle RTSP connection from a client.
|
|
13852
13943
|
*/
|
|
@@ -14011,6 +14102,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14011
14102
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
14012
14103
|
});
|
|
14013
14104
|
} else if (method === "DESCRIBE") {
|
|
14105
|
+
if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
|
|
14106
|
+
void this.api.ensureConnected().catch(() => {
|
|
14107
|
+
});
|
|
14108
|
+
}
|
|
14014
14109
|
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
14015
14110
|
try {
|
|
14016
14111
|
if (!this.nativeStreamActive) {
|
|
@@ -14048,6 +14143,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14048
14143
|
}
|
|
14049
14144
|
}
|
|
14050
14145
|
}
|
|
14146
|
+
if (!this.hasAudio && this.firstAudioPromise) {
|
|
14147
|
+
const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
|
|
14148
|
+
const audioPrimingStart = Date.now();
|
|
14149
|
+
try {
|
|
14150
|
+
await Promise.race([
|
|
14151
|
+
this.firstAudioPromise,
|
|
14152
|
+
new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
|
|
14153
|
+
]);
|
|
14154
|
+
} catch {
|
|
14155
|
+
}
|
|
14156
|
+
const audioPrimingElapsed = Date.now() - audioPrimingStart;
|
|
14157
|
+
if (this.hasAudio) {
|
|
14158
|
+
this.logger.info(
|
|
14159
|
+
`[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
|
|
14160
|
+
);
|
|
14161
|
+
} else {
|
|
14162
|
+
this.logger.info(
|
|
14163
|
+
`[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
|
|
14164
|
+
);
|
|
14165
|
+
}
|
|
14166
|
+
}
|
|
14051
14167
|
{
|
|
14052
14168
|
const { fmtp, hasParamSets: hasParamSets2 } = this.flow.getFmtp();
|
|
14053
14169
|
const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
|
|
@@ -14056,12 +14172,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14056
14172
|
);
|
|
14057
14173
|
}
|
|
14058
14174
|
const sdp = this.generateSdp();
|
|
14175
|
+
const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
|
|
14059
14176
|
sendResponse(
|
|
14060
14177
|
200,
|
|
14061
14178
|
"OK",
|
|
14062
14179
|
{
|
|
14063
14180
|
"Content-Type": "application/sdp",
|
|
14064
|
-
"Content-Base": `rtsp://${
|
|
14181
|
+
"Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
|
|
14065
14182
|
},
|
|
14066
14183
|
sdp
|
|
14067
14184
|
);
|
|
@@ -14084,7 +14201,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14084
14201
|
this.emit("client", clientId);
|
|
14085
14202
|
this.clearNoClientAutoStopTimer();
|
|
14086
14203
|
if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
|
|
14087
|
-
|
|
14204
|
+
void this.startNativeStream();
|
|
14088
14205
|
}
|
|
14089
14206
|
const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
|
|
14090
14207
|
const transport = (transportMatch?.[1] ?? "").trim();
|
|
@@ -14218,12 +14335,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14218
14335
|
}
|
|
14219
14336
|
}
|
|
14220
14337
|
};
|
|
14338
|
+
const runProcessBuffer = () => {
|
|
14339
|
+
processBuffer().catch((err) => {
|
|
14340
|
+
this.logger.debug(
|
|
14341
|
+
`[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
|
|
14342
|
+
);
|
|
14343
|
+
try {
|
|
14344
|
+
socket.destroy();
|
|
14345
|
+
} catch {
|
|
14346
|
+
}
|
|
14347
|
+
});
|
|
14348
|
+
};
|
|
14221
14349
|
socket.on("data", (data) => {
|
|
14222
14350
|
buffer = Buffer.concat([buffer, data]);
|
|
14223
|
-
|
|
14351
|
+
runProcessBuffer();
|
|
14224
14352
|
});
|
|
14225
14353
|
if (buffer.includes("\r\n\r\n")) {
|
|
14226
|
-
|
|
14354
|
+
runProcessBuffer();
|
|
14227
14355
|
}
|
|
14228
14356
|
}
|
|
14229
14357
|
/**
|
|
@@ -15091,6 +15219,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15091
15219
|
if (this.nativeStreamActive) {
|
|
15092
15220
|
return;
|
|
15093
15221
|
}
|
|
15222
|
+
if (!this.api.isReady) {
|
|
15223
|
+
if (this.api.isClosed) {
|
|
15224
|
+
this.logger.warn?.(
|
|
15225
|
+
`[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
|
|
15226
|
+
);
|
|
15227
|
+
return;
|
|
15228
|
+
}
|
|
15229
|
+
try {
|
|
15230
|
+
this.logger.info?.(
|
|
15231
|
+
`[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
|
|
15232
|
+
);
|
|
15233
|
+
await this.api.ensureConnected();
|
|
15234
|
+
} catch (e) {
|
|
15235
|
+
this.logger.warn?.(
|
|
15236
|
+
`[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
|
|
15237
|
+
);
|
|
15238
|
+
return;
|
|
15239
|
+
}
|
|
15240
|
+
}
|
|
15094
15241
|
this.nativeStreamActive = true;
|
|
15095
15242
|
this.firstFrameReceived = false;
|
|
15096
15243
|
this.firstAudioDetected = false;
|
|
@@ -15125,13 +15272,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15125
15272
|
await this.flow.startKeepAlive(this.api);
|
|
15126
15273
|
this.nativeFanout = new NativeStreamFanout({
|
|
15127
15274
|
maxQueueItems: 200,
|
|
15128
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
15275
|
+
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
15129
15276
|
variant: this.variant,
|
|
15130
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
15277
|
+
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
15278
|
+
signal
|
|
15131
15279
|
}),
|
|
15132
15280
|
onFrame: (frame) => {
|
|
15133
15281
|
if (frame.audio) {
|
|
15134
|
-
if (!this.hasAudio &&
|
|
15282
|
+
if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
|
|
15135
15283
|
const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
|
|
15136
15284
|
if (info) {
|
|
15137
15285
|
this.hasAudio = true;
|
|
@@ -15180,6 +15328,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15180
15328
|
onEnd: () => {
|
|
15181
15329
|
if (!this.nativeStreamActive) return;
|
|
15182
15330
|
this.nativeStreamActive = false;
|
|
15331
|
+
this.clearNoFrameDeadlineTimer();
|
|
15332
|
+
const hadFrames = this.firstFrameReceived;
|
|
15183
15333
|
this.firstFrameReceived = false;
|
|
15184
15334
|
this.firstFramePromise = null;
|
|
15185
15335
|
this.firstFrameResolve = null;
|
|
@@ -15204,7 +15354,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15204
15354
|
} catch {
|
|
15205
15355
|
}
|
|
15206
15356
|
}
|
|
15207
|
-
if (this.connectedClients.size > 0) {
|
|
15357
|
+
if (this.connectedClients.size > 0 && hadFrames) {
|
|
15208
15358
|
this.logger.info(
|
|
15209
15359
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
15210
15360
|
);
|
|
@@ -15216,6 +15366,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15216
15366
|
}
|
|
15217
15367
|
});
|
|
15218
15368
|
this.nativeFanout.start();
|
|
15369
|
+
this.clearNoFrameDeadlineTimer();
|
|
15370
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
15371
|
+
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
15372
|
+
this.noFrameDeadlineTimer = void 0;
|
|
15373
|
+
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
15374
|
+
this.logger.info(
|
|
15375
|
+
`[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
|
|
15376
|
+
);
|
|
15377
|
+
void this.stopNativeStream();
|
|
15378
|
+
}
|
|
15379
|
+
}, this.nativeStreamNoFrameDeadlineMs);
|
|
15380
|
+
this.noFrameDeadlineTimer?.unref?.();
|
|
15381
|
+
}
|
|
15219
15382
|
this.clearNoClientAutoStopTimer();
|
|
15220
15383
|
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
15221
15384
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
@@ -15232,6 +15395,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15232
15395
|
markFirstFrameReceived() {
|
|
15233
15396
|
if (!this.firstFrameReceived && this.firstFrameResolve) {
|
|
15234
15397
|
this.firstFrameReceived = true;
|
|
15398
|
+
this.clearNoFrameDeadlineTimer();
|
|
15235
15399
|
this.rtspDebugLog(
|
|
15236
15400
|
`First frame received from camera for profile ${this.profile}`
|
|
15237
15401
|
);
|
|
@@ -15258,6 +15422,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
15258
15422
|
);
|
|
15259
15423
|
this.flow.stopKeepAlive();
|
|
15260
15424
|
this.clearNoClientAutoStopTimer();
|
|
15425
|
+
this.clearNoFrameDeadlineTimer();
|
|
15261
15426
|
this.nativeStreamActive = false;
|
|
15262
15427
|
this.firstFrameReceived = false;
|
|
15263
15428
|
this.firstFramePromise = null;
|
|
@@ -15474,149 +15639,17 @@ init_BcMediaAnnexBDecoder();
|
|
|
15474
15639
|
// src/baichuan/stream/MpegTsMuxer.ts
|
|
15475
15640
|
var TS_PACKET_SIZE = 188;
|
|
15476
15641
|
var TS_SYNC_BYTE = 71;
|
|
15477
|
-
var
|
|
15478
|
-
var
|
|
15479
|
-
var
|
|
15642
|
+
var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
|
|
15643
|
+
var PID_PAT = 0;
|
|
15644
|
+
var PID_PMT = 4096;
|
|
15645
|
+
var PID_VIDEO = 256;
|
|
15646
|
+
var PID_AUDIO = 257;
|
|
15480
15647
|
var STREAM_TYPE_H264 = 27;
|
|
15481
15648
|
var STREAM_TYPE_H265 = 36;
|
|
15482
|
-
var
|
|
15483
|
-
var
|
|
15484
|
-
var
|
|
15485
|
-
|
|
15486
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
15487
|
-
packet[0] = TS_SYNC_BYTE;
|
|
15488
|
-
packet[1] = 64 | PAT_PID >> 8 & 31;
|
|
15489
|
-
packet[2] = PAT_PID & 255;
|
|
15490
|
-
packet[3] = 16 | patCc & 15;
|
|
15491
|
-
patCc = patCc + 1 & 15;
|
|
15492
|
-
packet[4] = 0;
|
|
15493
|
-
let idx = 5;
|
|
15494
|
-
packet[idx++] = 0;
|
|
15495
|
-
packet[idx++] = 176;
|
|
15496
|
-
packet[idx++] = 13;
|
|
15497
|
-
packet[idx++] = 0;
|
|
15498
|
-
packet[idx++] = 1;
|
|
15499
|
-
packet[idx++] = 193;
|
|
15500
|
-
packet[idx++] = 0;
|
|
15501
|
-
packet[idx++] = 0;
|
|
15502
|
-
packet[idx++] = 0;
|
|
15503
|
-
packet[idx++] = 1;
|
|
15504
|
-
packet[idx++] = 224 | PMT_PID >> 8 & 31;
|
|
15505
|
-
packet[idx++] = PMT_PID & 255;
|
|
15506
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
15507
|
-
packet.writeUInt32BE(crc, idx);
|
|
15508
|
-
return packet;
|
|
15509
|
-
}
|
|
15510
|
-
function createPmt(streamType) {
|
|
15511
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
15512
|
-
packet[0] = TS_SYNC_BYTE;
|
|
15513
|
-
packet[1] = 64 | PMT_PID >> 8 & 31;
|
|
15514
|
-
packet[2] = PMT_PID & 255;
|
|
15515
|
-
packet[3] = 16 | pmtCc & 15;
|
|
15516
|
-
pmtCc = pmtCc + 1 & 15;
|
|
15517
|
-
packet[4] = 0;
|
|
15518
|
-
let idx = 5;
|
|
15519
|
-
packet[idx++] = 2;
|
|
15520
|
-
packet[idx++] = 176;
|
|
15521
|
-
packet[idx++] = 18;
|
|
15522
|
-
packet[idx++] = 0;
|
|
15523
|
-
packet[idx++] = 1;
|
|
15524
|
-
packet[idx++] = 193;
|
|
15525
|
-
packet[idx++] = 0;
|
|
15526
|
-
packet[idx++] = 0;
|
|
15527
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
15528
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
15529
|
-
packet[idx++] = 240;
|
|
15530
|
-
packet[idx++] = 0;
|
|
15531
|
-
packet[idx++] = streamType;
|
|
15532
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
15533
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
15534
|
-
packet[idx++] = 240;
|
|
15535
|
-
packet[idx++] = 0;
|
|
15536
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
15537
|
-
packet.writeUInt32BE(crc, idx);
|
|
15538
|
-
return packet;
|
|
15539
|
-
}
|
|
15540
|
-
function createVideoPes(data, pts, isKeyframe) {
|
|
15541
|
-
const packets = [];
|
|
15542
|
-
const pts90k = Math.floor(pts * 9e4 / 1e6);
|
|
15543
|
-
const pesHeaderLen = 14;
|
|
15544
|
-
const pesHeader = Buffer.alloc(pesHeaderLen);
|
|
15545
|
-
let idx = 0;
|
|
15546
|
-
pesHeader[idx++] = 0;
|
|
15547
|
-
pesHeader[idx++] = 0;
|
|
15548
|
-
pesHeader[idx++] = 1;
|
|
15549
|
-
pesHeader[idx++] = 224;
|
|
15550
|
-
pesHeader[idx++] = 0;
|
|
15551
|
-
pesHeader[idx++] = 0;
|
|
15552
|
-
pesHeader[idx++] = 128;
|
|
15553
|
-
pesHeader[idx++] = 128;
|
|
15554
|
-
pesHeader[idx++] = 5;
|
|
15555
|
-
pesHeader[idx++] = 33 | pts90k >> 29 & 14;
|
|
15556
|
-
pesHeader[idx++] = pts90k >> 22 & 255;
|
|
15557
|
-
pesHeader[idx++] = 1 | pts90k >> 14 & 254;
|
|
15558
|
-
pesHeader[idx++] = pts90k >> 7 & 255;
|
|
15559
|
-
pesHeader[idx++] = 1 | pts90k << 1 & 254;
|
|
15560
|
-
const pesData = Buffer.concat([pesHeader, data]);
|
|
15561
|
-
let pesOffset = 0;
|
|
15562
|
-
let isFirst = true;
|
|
15563
|
-
while (pesOffset < pesData.length) {
|
|
15564
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
15565
|
-
let pktIdx = 0;
|
|
15566
|
-
packet[pktIdx++] = TS_SYNC_BYTE;
|
|
15567
|
-
packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
|
|
15568
|
-
packet[pktIdx++] = VIDEO_PID & 255;
|
|
15569
|
-
const remaining = pesData.length - pesOffset;
|
|
15570
|
-
const maxPayload = TS_PACKET_SIZE - 4;
|
|
15571
|
-
if (remaining >= maxPayload) {
|
|
15572
|
-
packet[pktIdx++] = 16 | videoCc & 15;
|
|
15573
|
-
videoCc = videoCc + 1 & 15;
|
|
15574
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
|
|
15575
|
-
pesOffset += maxPayload;
|
|
15576
|
-
} else {
|
|
15577
|
-
const adaptLen = maxPayload - remaining - 1;
|
|
15578
|
-
if (adaptLen < 0) {
|
|
15579
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
15580
|
-
videoCc = videoCc + 1 & 15;
|
|
15581
|
-
packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
|
|
15582
|
-
if (isFirst && isKeyframe) {
|
|
15583
|
-
packet[pktIdx++] = 64;
|
|
15584
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
15585
|
-
packet[i] = 255;
|
|
15586
|
-
}
|
|
15587
|
-
} else {
|
|
15588
|
-
packet[pktIdx++] = 0;
|
|
15589
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
15590
|
-
packet[i] = 255;
|
|
15591
|
-
}
|
|
15592
|
-
}
|
|
15593
|
-
pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
|
|
15594
|
-
pesOffset += remaining;
|
|
15595
|
-
} else {
|
|
15596
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
15597
|
-
videoCc = videoCc + 1 & 15;
|
|
15598
|
-
if (adaptLen === 0) {
|
|
15599
|
-
packet[pktIdx++] = 0;
|
|
15600
|
-
} else {
|
|
15601
|
-
packet[pktIdx++] = adaptLen;
|
|
15602
|
-
if (isFirst && isKeyframe) {
|
|
15603
|
-
packet[pktIdx++] = 64;
|
|
15604
|
-
} else {
|
|
15605
|
-
packet[pktIdx++] = 0;
|
|
15606
|
-
}
|
|
15607
|
-
for (let i = 0; i < adaptLen - 1; i++) {
|
|
15608
|
-
packet[pktIdx++] = 255;
|
|
15609
|
-
}
|
|
15610
|
-
}
|
|
15611
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
|
|
15612
|
-
pesOffset += remaining;
|
|
15613
|
-
}
|
|
15614
|
-
}
|
|
15615
|
-
packets.push(packet);
|
|
15616
|
-
isFirst = false;
|
|
15617
|
-
}
|
|
15618
|
-
return packets;
|
|
15619
|
-
}
|
|
15649
|
+
var STREAM_TYPE_AAC = 15;
|
|
15650
|
+
var PES_STREAM_ID_VIDEO = 224;
|
|
15651
|
+
var PES_STREAM_ID_AUDIO = 192;
|
|
15652
|
+
var PAT_PMT_INTERVAL = 40;
|
|
15620
15653
|
function crc32Mpeg(data) {
|
|
15621
15654
|
let crc = 4294967295;
|
|
15622
15655
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -15631,45 +15664,218 @@ function crc32Mpeg(data) {
|
|
|
15631
15664
|
}
|
|
15632
15665
|
return crc >>> 0;
|
|
15633
15666
|
}
|
|
15667
|
+
function usToPts(us) {
|
|
15668
|
+
return Math.floor(us * 90 / 1e3) & 8589934591;
|
|
15669
|
+
}
|
|
15670
|
+
function encodePts(buf, offset, pts, prefix) {
|
|
15671
|
+
buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
|
|
15672
|
+
buf[offset + 1] = pts >>> 22 & 255;
|
|
15673
|
+
buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
|
|
15674
|
+
buf[offset + 3] = pts >>> 7 & 255;
|
|
15675
|
+
buf[offset + 4] = (pts & 127) << 1 | 1;
|
|
15676
|
+
}
|
|
15677
|
+
function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
|
|
15678
|
+
buf[0] = TS_SYNC_BYTE;
|
|
15679
|
+
buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
|
|
15680
|
+
buf[2] = pid & 255;
|
|
15681
|
+
buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
|
|
15682
|
+
}
|
|
15683
|
+
function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
|
|
15684
|
+
const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
|
|
15685
|
+
const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
|
|
15686
|
+
let pesOffset = 0;
|
|
15687
|
+
let outOffset = 0;
|
|
15688
|
+
let isFirst = true;
|
|
15689
|
+
while (pesOffset < pesData.length) {
|
|
15690
|
+
const remaining = pesData.length - pesOffset;
|
|
15691
|
+
const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
|
|
15692
|
+
outOffset += TS_PACKET_SIZE;
|
|
15693
|
+
if (remaining >= TS_PAYLOAD_SIZE) {
|
|
15694
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
|
|
15695
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
15696
|
+
pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
|
|
15697
|
+
pesOffset += TS_PAYLOAD_SIZE;
|
|
15698
|
+
} else {
|
|
15699
|
+
const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
|
|
15700
|
+
if (paddingNeeded === 1) {
|
|
15701
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
15702
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
15703
|
+
packet[4] = 0;
|
|
15704
|
+
pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
|
|
15705
|
+
} else {
|
|
15706
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
15707
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
15708
|
+
const adaptLen = paddingNeeded - 1;
|
|
15709
|
+
packet[4] = adaptLen;
|
|
15710
|
+
packet[5] = isFirst && isKeyframe ? 64 : 0;
|
|
15711
|
+
packet.fill(255, 6, 4 + paddingNeeded);
|
|
15712
|
+
pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
|
|
15713
|
+
}
|
|
15714
|
+
pesOffset += remaining;
|
|
15715
|
+
}
|
|
15716
|
+
isFirst = false;
|
|
15717
|
+
}
|
|
15718
|
+
return out;
|
|
15719
|
+
}
|
|
15720
|
+
function buildPat(cc) {
|
|
15721
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
15722
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
15723
|
+
pkt[1] = 64 | PID_PAT >> 8 & 31;
|
|
15724
|
+
pkt[2] = PID_PAT & 255;
|
|
15725
|
+
pkt[3] = 16 | cc & 15;
|
|
15726
|
+
pkt[4] = 0;
|
|
15727
|
+
const sectionStart = 5;
|
|
15728
|
+
let i = sectionStart;
|
|
15729
|
+
pkt[i++] = 0;
|
|
15730
|
+
pkt[i++] = 176;
|
|
15731
|
+
pkt[i++] = 13;
|
|
15732
|
+
pkt[i++] = 0;
|
|
15733
|
+
pkt[i++] = 1;
|
|
15734
|
+
pkt[i++] = 193;
|
|
15735
|
+
pkt[i++] = 0;
|
|
15736
|
+
pkt[i++] = 0;
|
|
15737
|
+
pkt[i++] = 0;
|
|
15738
|
+
pkt[i++] = 1;
|
|
15739
|
+
pkt[i++] = 224 | PID_PMT >> 8 & 31;
|
|
15740
|
+
pkt[i++] = PID_PMT & 255;
|
|
15741
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
15742
|
+
pkt.writeUInt32BE(crc, i);
|
|
15743
|
+
return pkt;
|
|
15744
|
+
}
|
|
15745
|
+
function buildPmt(videoStreamType, includeAudio, cc) {
|
|
15746
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
15747
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
15748
|
+
pkt[1] = 64 | PID_PMT >> 8 & 31;
|
|
15749
|
+
pkt[2] = PID_PMT & 255;
|
|
15750
|
+
pkt[3] = 16 | cc & 15;
|
|
15751
|
+
pkt[4] = 0;
|
|
15752
|
+
const sectionStart = 5;
|
|
15753
|
+
let i = sectionStart;
|
|
15754
|
+
pkt[i++] = 2;
|
|
15755
|
+
pkt[i++] = 176;
|
|
15756
|
+
const sectionLenPos = i;
|
|
15757
|
+
i += 1;
|
|
15758
|
+
pkt[i++] = 0;
|
|
15759
|
+
pkt[i++] = 1;
|
|
15760
|
+
pkt[i++] = 193;
|
|
15761
|
+
pkt[i++] = 0;
|
|
15762
|
+
pkt[i++] = 0;
|
|
15763
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
15764
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
15765
|
+
pkt[i++] = 240;
|
|
15766
|
+
pkt[i++] = 0;
|
|
15767
|
+
pkt[i++] = videoStreamType;
|
|
15768
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
15769
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
15770
|
+
pkt[i++] = 240;
|
|
15771
|
+
pkt[i++] = 0;
|
|
15772
|
+
if (includeAudio) {
|
|
15773
|
+
pkt[i++] = STREAM_TYPE_AAC;
|
|
15774
|
+
pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
|
|
15775
|
+
pkt[i++] = PID_AUDIO & 255;
|
|
15776
|
+
pkt[i++] = 240;
|
|
15777
|
+
pkt[i++] = 0;
|
|
15778
|
+
}
|
|
15779
|
+
const sectionLen = i - sectionStart - 3 + 4;
|
|
15780
|
+
pkt[sectionLenPos] = sectionLen;
|
|
15781
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
15782
|
+
pkt.writeUInt32BE(crc, i);
|
|
15783
|
+
return pkt;
|
|
15784
|
+
}
|
|
15785
|
+
function buildVideoPes(annexBData, ptsUs, isKeyframe) {
|
|
15786
|
+
const pts = usToPts(ptsUs);
|
|
15787
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
15788
|
+
pesHeader[0] = 0;
|
|
15789
|
+
pesHeader[1] = 0;
|
|
15790
|
+
pesHeader[2] = 1;
|
|
15791
|
+
pesHeader[3] = PES_STREAM_ID_VIDEO;
|
|
15792
|
+
pesHeader[4] = 0;
|
|
15793
|
+
pesHeader[5] = 0;
|
|
15794
|
+
pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
|
|
15795
|
+
pesHeader[7] = 128;
|
|
15796
|
+
pesHeader[8] = 5;
|
|
15797
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
15798
|
+
return Buffer.concat([pesHeader, annexBData]);
|
|
15799
|
+
}
|
|
15800
|
+
function buildAudioPes(adtsData, ptsUs) {
|
|
15801
|
+
const pts = usToPts(ptsUs);
|
|
15802
|
+
const pesPayloadLen = 8 + adtsData.length;
|
|
15803
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
15804
|
+
pesHeader[0] = 0;
|
|
15805
|
+
pesHeader[1] = 0;
|
|
15806
|
+
pesHeader[2] = 1;
|
|
15807
|
+
pesHeader[3] = PES_STREAM_ID_AUDIO;
|
|
15808
|
+
pesHeader[4] = pesPayloadLen >> 8 & 255;
|
|
15809
|
+
pesHeader[5] = pesPayloadLen & 255;
|
|
15810
|
+
pesHeader[6] = 128;
|
|
15811
|
+
pesHeader[7] = 128;
|
|
15812
|
+
pesHeader[8] = 5;
|
|
15813
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
15814
|
+
return Buffer.concat([pesHeader, adtsData]);
|
|
15815
|
+
}
|
|
15634
15816
|
var MpegTsMuxer = class {
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15817
|
+
videoStreamType;
|
|
15818
|
+
includeAudio;
|
|
15819
|
+
// Per-instance continuity counters (4-bit, wrap at 16)
|
|
15820
|
+
patCc = 0;
|
|
15821
|
+
pmtCc = 0;
|
|
15822
|
+
videoCc = 0;
|
|
15823
|
+
audioCc = 0;
|
|
15824
|
+
framesSinceTableSend = 0;
|
|
15825
|
+
tablesSent = false;
|
|
15641
15826
|
constructor(options) {
|
|
15642
|
-
this.
|
|
15643
|
-
|
|
15644
|
-
/**
|
|
15645
|
-
* Reset continuity counters (call when starting a new stream).
|
|
15646
|
-
*/
|
|
15647
|
-
static resetCounters() {
|
|
15648
|
-
patCc = 0;
|
|
15649
|
-
pmtCc = 0;
|
|
15650
|
-
videoCc = 0;
|
|
15827
|
+
this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
15828
|
+
this.includeAudio = options.includeAudio ?? true;
|
|
15651
15829
|
}
|
|
15652
15830
|
/**
|
|
15653
|
-
* Mux a video frame into MPEG-TS packets.
|
|
15831
|
+
* Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
|
|
15832
|
+
* PAT and PMT are emitted before keyframes and periodically.
|
|
15654
15833
|
*
|
|
15655
|
-
* @param
|
|
15656
|
-
* @param
|
|
15657
|
-
* @param isKeyframe - Whether this is
|
|
15658
|
-
* @returns Buffer containing all TS packets for this frame
|
|
15834
|
+
* @param annexBData - Annex-B video data (with start codes)
|
|
15835
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
15836
|
+
* @param isKeyframe - Whether this is an IDR / IRAP frame
|
|
15659
15837
|
*/
|
|
15660
|
-
|
|
15661
|
-
const
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
this.
|
|
15666
|
-
this.
|
|
15667
|
-
this.
|
|
15668
|
-
|
|
15669
|
-
|
|
15670
|
-
|
|
15671
|
-
|
|
15672
|
-
|
|
15838
|
+
muxVideo(annexBData, ptsUs, isKeyframe) {
|
|
15839
|
+
const chunks = [];
|
|
15840
|
+
const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
|
|
15841
|
+
if (needTables) {
|
|
15842
|
+
chunks.push(buildPat(this.patCc));
|
|
15843
|
+
this.patCc = this.patCc + 1 & 15;
|
|
15844
|
+
chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
|
|
15845
|
+
this.pmtCc = this.pmtCc + 1 & 15;
|
|
15846
|
+
this.tablesSent = true;
|
|
15847
|
+
this.framesSinceTableSend = 0;
|
|
15848
|
+
}
|
|
15849
|
+
this.framesSinceTableSend++;
|
|
15850
|
+
const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
|
|
15851
|
+
const ccRef = { cc: this.videoCc };
|
|
15852
|
+
chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
|
|
15853
|
+
this.videoCc = ccRef.cc;
|
|
15854
|
+
return Buffer.concat(chunks);
|
|
15855
|
+
}
|
|
15856
|
+
/**
|
|
15857
|
+
* Mux an audio frame (ADTS AAC) into MPEG-TS packets.
|
|
15858
|
+
* Returns an empty Buffer when includeAudio is false.
|
|
15859
|
+
*
|
|
15860
|
+
* @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
|
|
15861
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
15862
|
+
*/
|
|
15863
|
+
muxAudio(adtsData, ptsUs) {
|
|
15864
|
+
if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
|
|
15865
|
+
const pes = buildAudioPes(adtsData, ptsUs);
|
|
15866
|
+
const ccRef = { cc: this.audioCc };
|
|
15867
|
+
const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
|
|
15868
|
+
this.audioCc = ccRef.cc;
|
|
15869
|
+
return result;
|
|
15870
|
+
}
|
|
15871
|
+
/** Reset all continuity counters and table state (e.g. after stream restart). */
|
|
15872
|
+
reset() {
|
|
15873
|
+
this.patCc = 0;
|
|
15874
|
+
this.pmtCc = 0;
|
|
15875
|
+
this.videoCc = 0;
|
|
15876
|
+
this.audioCc = 0;
|
|
15877
|
+
this.framesSinceTableSend = 0;
|
|
15878
|
+
this.tablesSent = false;
|
|
15673
15879
|
}
|
|
15674
15880
|
};
|
|
15675
15881
|
|
|
@@ -16103,6 +16309,53 @@ var getAiStateViaGetAiAlarm = async (params) => {
|
|
|
16103
16309
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "getAiState failed"));
|
|
16104
16310
|
};
|
|
16105
16311
|
|
|
16312
|
+
// src/reolink/baichuan/utils/sleepInference.ts
|
|
16313
|
+
function decideSleepInferenceTransition(input) {
|
|
16314
|
+
const { inferred, committed, pending, hysteresisPolls } = input;
|
|
16315
|
+
if (committed === void 0) {
|
|
16316
|
+
return {
|
|
16317
|
+
emit: inferred === "sleeping" ? "sleeping" : null,
|
|
16318
|
+
nextCommitted: inferred,
|
|
16319
|
+
nextPending: void 0
|
|
16320
|
+
};
|
|
16321
|
+
}
|
|
16322
|
+
if (inferred === committed) {
|
|
16323
|
+
return {
|
|
16324
|
+
emit: null,
|
|
16325
|
+
nextCommitted: committed,
|
|
16326
|
+
nextPending: void 0
|
|
16327
|
+
};
|
|
16328
|
+
}
|
|
16329
|
+
const effectivePolls = Math.max(1, hysteresisPolls);
|
|
16330
|
+
if (!pending || pending.state !== inferred) {
|
|
16331
|
+
if (effectivePolls <= 1) {
|
|
16332
|
+
return {
|
|
16333
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
16334
|
+
nextCommitted: inferred,
|
|
16335
|
+
nextPending: void 0
|
|
16336
|
+
};
|
|
16337
|
+
}
|
|
16338
|
+
return {
|
|
16339
|
+
emit: null,
|
|
16340
|
+
nextCommitted: committed,
|
|
16341
|
+
nextPending: { state: inferred, count: 1 }
|
|
16342
|
+
};
|
|
16343
|
+
}
|
|
16344
|
+
const nextCount = pending.count + 1;
|
|
16345
|
+
if (nextCount < effectivePolls) {
|
|
16346
|
+
return {
|
|
16347
|
+
emit: null,
|
|
16348
|
+
nextCommitted: committed,
|
|
16349
|
+
nextPending: { state: inferred, count: nextCount }
|
|
16350
|
+
};
|
|
16351
|
+
}
|
|
16352
|
+
return {
|
|
16353
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
16354
|
+
nextCommitted: inferred,
|
|
16355
|
+
nextPending: void 0
|
|
16356
|
+
};
|
|
16357
|
+
}
|
|
16358
|
+
|
|
16106
16359
|
// src/reolink/baichuan/utils/channelInfoPush.ts
|
|
16107
16360
|
init_xml();
|
|
16108
16361
|
var parseOptionalInt = (value) => {
|
|
@@ -18072,6 +18325,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18072
18325
|
* - "replay:XXX" - dedicated per replay session
|
|
18073
18326
|
*/
|
|
18074
18327
|
socketPool = /* @__PURE__ */ new Map();
|
|
18328
|
+
/**
|
|
18329
|
+
* Consecutive stream-start (cmdId=3) timeout counter per socket tag.
|
|
18330
|
+
* When a streaming socket has N consecutive timeouts, the socket is force-closed
|
|
18331
|
+
* so the next attempt creates a fresh connection. Resets on success.
|
|
18332
|
+
*/
|
|
18333
|
+
consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
|
|
18334
|
+
static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
|
|
18075
18335
|
/** BaichuanClientOptions to use when creating new sockets */
|
|
18076
18336
|
clientOptions;
|
|
18077
18337
|
/**
|
|
@@ -18226,14 +18486,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18226
18486
|
if (!xml) return;
|
|
18227
18487
|
const channel = frame.header.channelId;
|
|
18228
18488
|
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
18229
|
-
if (battery.batteryPercent
|
|
18230
|
-
|
|
18231
|
-
type: "battery",
|
|
18232
|
-
channel,
|
|
18233
|
-
timestamp: Date.now(),
|
|
18234
|
-
battery
|
|
18235
|
-
});
|
|
18489
|
+
if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
|
|
18490
|
+
return;
|
|
18236
18491
|
}
|
|
18492
|
+
const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
|
|
18493
|
+
if (this.lastBatteryPushKey.get(channel) === key) {
|
|
18494
|
+
return;
|
|
18495
|
+
}
|
|
18496
|
+
this.lastBatteryPushKey.set(channel, key);
|
|
18497
|
+
this.dispatchSimpleEvent({
|
|
18498
|
+
type: "battery",
|
|
18499
|
+
channel,
|
|
18500
|
+
timestamp: Date.now(),
|
|
18501
|
+
battery
|
|
18502
|
+
});
|
|
18237
18503
|
} catch (e) {
|
|
18238
18504
|
this.logger.debug?.(
|
|
18239
18505
|
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
@@ -18366,7 +18632,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18366
18632
|
statePollingInterval;
|
|
18367
18633
|
udpSleepInferenceInterval;
|
|
18368
18634
|
udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
18635
|
+
/**
|
|
18636
|
+
* Per-channel pending sleep-state candidate for hysteresis.
|
|
18637
|
+
* When the inference flips to a new state we require N consecutive polls
|
|
18638
|
+
* of that same state before committing it — this filters out transient
|
|
18639
|
+
* flapping caused by non-waking traffic drifting in/out of the 10 s
|
|
18640
|
+
* getSleepStatus() observation window during stream teardown.
|
|
18641
|
+
*/
|
|
18642
|
+
udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
18369
18643
|
udpSleepInferenceIntervalMs = 2e3;
|
|
18644
|
+
/** Consecutive inference polls required to commit a new sleeping/awake state. */
|
|
18645
|
+
udpSleepInferenceHysteresisPolls = 2;
|
|
18370
18646
|
lastMotionState;
|
|
18371
18647
|
lastAiState;
|
|
18372
18648
|
aiStatePollingDisabled = false;
|
|
@@ -18399,6 +18675,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18399
18675
|
deviceCapabilitiesCache = /* @__PURE__ */ new Map();
|
|
18400
18676
|
static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
18401
18677
|
// 5 minutes
|
|
18678
|
+
/**
|
|
18679
|
+
* Dedupe key for battery push events (cmd_id 252), per channel.
|
|
18680
|
+
* Cameras emit BatteryInfoList frequently while streaming (every few
|
|
18681
|
+
* seconds). We only forward an event when the meaningful fields change
|
|
18682
|
+
* (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
|
|
18683
|
+
* consumers and the UI event log.
|
|
18684
|
+
*/
|
|
18685
|
+
lastBatteryPushKey = /* @__PURE__ */ new Map();
|
|
18402
18686
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18403
18687
|
// SOCKET POOL CONSTANTS
|
|
18404
18688
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -18825,6 +19109,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18825
19109
|
*/
|
|
18826
19110
|
attachD2cDiscListener(client) {
|
|
18827
19111
|
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
19112
|
+
client.on("error", () => {
|
|
19113
|
+
});
|
|
18828
19114
|
}
|
|
18829
19115
|
/**
|
|
18830
19116
|
* Acquire a socket from the pool by tag.
|
|
@@ -18943,6 +19229,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18943
19229
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18944
19230
|
const newClient = new BaichuanClient(clientOpts);
|
|
18945
19231
|
this.attachD2cDiscListener(newClient);
|
|
19232
|
+
newClient.on("error", (err) => {
|
|
19233
|
+
log?.debug?.(
|
|
19234
|
+
`[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
|
|
19235
|
+
);
|
|
19236
|
+
});
|
|
18946
19237
|
await newClient.login();
|
|
18947
19238
|
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18948
19239
|
if (existingCooldown) {
|
|
@@ -19458,6 +19749,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19458
19749
|
* Only counts sessions from our own IP address.
|
|
19459
19750
|
*/
|
|
19460
19751
|
async maybeRebootOnTooManySessions() {
|
|
19752
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
19461
19753
|
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
19462
19754
|
if (this.sessionGuardRebootInFlight) return;
|
|
19463
19755
|
const cooldownMs = 10 * 6e4;
|
|
@@ -19899,6 +20191,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19899
20191
|
}
|
|
19900
20192
|
async renewSimpleEventSubscription() {
|
|
19901
20193
|
if (this.simpleEventListeners.size === 0) return;
|
|
20194
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
19902
20195
|
if (this.simpleEventResubscribeInFlight)
|
|
19903
20196
|
return await this.simpleEventResubscribeInFlight;
|
|
19904
20197
|
this.simpleEventResubscribeInFlight = (async () => {
|
|
@@ -23666,23 +23959,32 @@ ${stderr}`)
|
|
|
23666
23959
|
return;
|
|
23667
23960
|
}
|
|
23668
23961
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
23962
|
+
if (!this.client.isSocketConnected?.()) {
|
|
23963
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23964
|
+
return;
|
|
23965
|
+
}
|
|
23669
23966
|
const status = this.getSleepStatus({ channel });
|
|
23670
23967
|
if (status.state === "unknown") return;
|
|
23671
|
-
const
|
|
23672
|
-
this.
|
|
23673
|
-
|
|
23674
|
-
|
|
23675
|
-
|
|
23676
|
-
|
|
23677
|
-
|
|
23678
|
-
|
|
23679
|
-
|
|
23680
|
-
|
|
23681
|
-
|
|
23968
|
+
const committed = this.udpLastInferredSleepStateByChannel.get(channel);
|
|
23969
|
+
const pending = this.udpPendingSleepStateByChannel.get(channel);
|
|
23970
|
+
const decision = decideSleepInferenceTransition({
|
|
23971
|
+
inferred: status.state,
|
|
23972
|
+
committed,
|
|
23973
|
+
pending,
|
|
23974
|
+
hysteresisPolls: this.udpSleepInferenceHysteresisPolls
|
|
23975
|
+
});
|
|
23976
|
+
this.udpLastInferredSleepStateByChannel.set(
|
|
23977
|
+
channel,
|
|
23978
|
+
decision.nextCommitted
|
|
23979
|
+
);
|
|
23980
|
+
if (decision.nextPending === void 0) {
|
|
23981
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23982
|
+
} else {
|
|
23983
|
+
this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
|
|
23682
23984
|
}
|
|
23683
|
-
if (
|
|
23985
|
+
if (decision.emit) {
|
|
23684
23986
|
this.dispatchSimpleEvent({
|
|
23685
|
-
type:
|
|
23987
|
+
type: decision.emit,
|
|
23686
23988
|
channel,
|
|
23687
23989
|
timestamp: Date.now()
|
|
23688
23990
|
});
|
|
@@ -23705,6 +24007,7 @@ ${stderr}`)
|
|
|
23705
24007
|
this.udpSleepInferenceInterval = void 0;
|
|
23706
24008
|
}
|
|
23707
24009
|
this.udpLastInferredSleepStateByChannel.clear();
|
|
24010
|
+
this.udpPendingSleepStateByChannel.clear();
|
|
23708
24011
|
}
|
|
23709
24012
|
/**
|
|
23710
24013
|
* GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
|
|
@@ -23871,6 +24174,7 @@ ${stderr}`)
|
|
|
23871
24174
|
`${ch}:${profile}:${variant}`,
|
|
23872
24175
|
frame.header.msgNum
|
|
23873
24176
|
);
|
|
24177
|
+
this.resetStreamTimeoutCounter(targetClient);
|
|
23874
24178
|
return;
|
|
23875
24179
|
} catch (error) {
|
|
23876
24180
|
lastError = error;
|
|
@@ -23885,6 +24189,10 @@ ${stderr}`)
|
|
|
23885
24189
|
}
|
|
23886
24190
|
}
|
|
23887
24191
|
}
|
|
24192
|
+
const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
|
|
24193
|
+
if (isTimeout) {
|
|
24194
|
+
this.trackStreamTimeout(targetClient);
|
|
24195
|
+
}
|
|
23888
24196
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
23889
24197
|
}
|
|
23890
24198
|
/**
|
|
@@ -24344,6 +24652,18 @@ ${stderr}`)
|
|
|
24344
24652
|
notifyD2cDisc() {
|
|
24345
24653
|
const now = Date.now();
|
|
24346
24654
|
this.lastD2cDiscAtMs = now;
|
|
24655
|
+
const streamingTags = Array.from(this.socketPool.keys()).filter(
|
|
24656
|
+
(tag) => tag.startsWith("streaming:")
|
|
24657
|
+
);
|
|
24658
|
+
if (streamingTags.length > 0) {
|
|
24659
|
+
this.logger?.log?.(
|
|
24660
|
+
`[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
|
|
24661
|
+
);
|
|
24662
|
+
for (const tag of streamingTags) {
|
|
24663
|
+
this.forceClosePooledSocket(tag, this.logger).catch(() => {
|
|
24664
|
+
});
|
|
24665
|
+
}
|
|
24666
|
+
}
|
|
24347
24667
|
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
24348
24668
|
const existing = this.socketPoolCooldowns.get(this.host);
|
|
24349
24669
|
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
@@ -24376,6 +24696,43 @@ ${stderr}`)
|
|
|
24376
24696
|
}
|
|
24377
24697
|
}
|
|
24378
24698
|
}
|
|
24699
|
+
/**
|
|
24700
|
+
* Find the socket pool tag for a given BaichuanClient instance.
|
|
24701
|
+
* Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
|
|
24702
|
+
*/
|
|
24703
|
+
findSocketTagForClient(client) {
|
|
24704
|
+
for (const [tag, entry] of this.socketPool) {
|
|
24705
|
+
if (entry.client === client) return tag;
|
|
24706
|
+
}
|
|
24707
|
+
return void 0;
|
|
24708
|
+
}
|
|
24709
|
+
/**
|
|
24710
|
+
* Reset the consecutive stream-start timeout counter for a streaming socket.
|
|
24711
|
+
* Called on successful stream start.
|
|
24712
|
+
*/
|
|
24713
|
+
resetStreamTimeoutCounter(client) {
|
|
24714
|
+
const tag = this.findSocketTagForClient(client);
|
|
24715
|
+
if (tag) this.consecutiveStreamTimeouts.delete(tag);
|
|
24716
|
+
}
|
|
24717
|
+
/**
|
|
24718
|
+
* Track a stream-start timeout on a streaming socket.
|
|
24719
|
+
* After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
|
|
24720
|
+
* socket so the next attempt creates a fresh connection.
|
|
24721
|
+
*/
|
|
24722
|
+
trackStreamTimeout(client) {
|
|
24723
|
+
const tag = this.findSocketTagForClient(client);
|
|
24724
|
+
if (!tag || !tag.startsWith("streaming:")) return;
|
|
24725
|
+
const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
|
|
24726
|
+
this.consecutiveStreamTimeouts.set(tag, count);
|
|
24727
|
+
if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
|
|
24728
|
+
this.logger?.warn?.(
|
|
24729
|
+
`[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
|
|
24730
|
+
);
|
|
24731
|
+
this.consecutiveStreamTimeouts.delete(tag);
|
|
24732
|
+
this.forceClosePooledSocket(tag, this.logger).catch(() => {
|
|
24733
|
+
});
|
|
24734
|
+
}
|
|
24735
|
+
}
|
|
24379
24736
|
/**
|
|
24380
24737
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
24381
24738
|
*
|
|
@@ -27249,8 +27606,8 @@ ${scheduleItems}
|
|
|
27249
27606
|
);
|
|
27250
27607
|
let args;
|
|
27251
27608
|
if (useMpegTsMuxer) {
|
|
27252
|
-
MpegTsMuxer
|
|
27253
|
-
tsMuxer
|
|
27609
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
27610
|
+
tsMuxer.reset();
|
|
27254
27611
|
args = [
|
|
27255
27612
|
"-hide_banner",
|
|
27256
27613
|
"-loglevel",
|
|
@@ -27414,7 +27771,7 @@ ${scheduleItems}
|
|
|
27414
27771
|
startFfmpeg(videoType);
|
|
27415
27772
|
frameCount++;
|
|
27416
27773
|
if (useMpegTsMuxer && tsMuxer) {
|
|
27417
|
-
const tsData = tsMuxer.
|
|
27774
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
27418
27775
|
input.write(tsData);
|
|
27419
27776
|
} else {
|
|
27420
27777
|
if (videoType === "H264") input.write(H264_AUD);
|
|
@@ -27703,8 +28060,8 @@ ${scheduleItems}
|
|
|
27703
28060
|
logger?.log?.(
|
|
27704
28061
|
`[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
|
|
27705
28062
|
);
|
|
27706
|
-
MpegTsMuxer
|
|
27707
|
-
tsMuxer
|
|
28063
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
28064
|
+
tsMuxer.reset();
|
|
27708
28065
|
const args = [
|
|
27709
28066
|
"-hide_banner",
|
|
27710
28067
|
"-loglevel",
|
|
@@ -27869,7 +28226,7 @@ ${scheduleItems}
|
|
|
27869
28226
|
startFfmpeg(videoType);
|
|
27870
28227
|
frameCount++;
|
|
27871
28228
|
if (tsMuxer) {
|
|
27872
|
-
const tsData = tsMuxer.
|
|
28229
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
27873
28230
|
input.write(tsData);
|
|
27874
28231
|
}
|
|
27875
28232
|
if (frameCount === 1) {
|
|
@@ -33640,6 +33997,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33640
33997
|
gracePeriodMs;
|
|
33641
33998
|
prebufferMaxMs;
|
|
33642
33999
|
maxBufferBytes;
|
|
34000
|
+
streamTimeoutMs;
|
|
33643
34001
|
prestartStream;
|
|
33644
34002
|
active = false;
|
|
33645
34003
|
server;
|
|
@@ -33653,8 +34011,16 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33653
34011
|
connectedClients = /* @__PURE__ */ new Set();
|
|
33654
34012
|
clientSockets = /* @__PURE__ */ new Map();
|
|
33655
34013
|
stopGraceTimer;
|
|
34014
|
+
// Stream health monitoring
|
|
34015
|
+
lastFrameAt = 0;
|
|
34016
|
+
streamHealthTimer;
|
|
34017
|
+
totalFramesReceived = 0;
|
|
34018
|
+
totalVideoFramesWritten = 0;
|
|
33656
34019
|
// Prebuffer
|
|
33657
34020
|
prebuffer = [];
|
|
34021
|
+
// Audio metadata — populated on first valid ADTS AAC frame.
|
|
34022
|
+
// Exposed via getAudioInfo() for the stream-diagnostics feature.
|
|
34023
|
+
audioInfo = null;
|
|
33658
34024
|
constructor(options) {
|
|
33659
34025
|
super();
|
|
33660
34026
|
this.api = options.api;
|
|
@@ -33668,6 +34034,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33668
34034
|
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
33669
34035
|
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
33670
34036
|
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
34037
|
+
this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
|
|
33671
34038
|
this.prestartStream = options.prestartStream ?? true;
|
|
33672
34039
|
}
|
|
33673
34040
|
// -----------------------------------------------------------------------
|
|
@@ -33706,6 +34073,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33706
34073
|
if (!this.active) return;
|
|
33707
34074
|
this.active = false;
|
|
33708
34075
|
clearTimeout(this.stopGraceTimer);
|
|
34076
|
+
this.stopStreamHealthMonitor();
|
|
33709
34077
|
for (const [id, sock] of this.clientSockets) {
|
|
33710
34078
|
sock.destroy();
|
|
33711
34079
|
this.connectedClients.delete(id);
|
|
@@ -33736,6 +34104,45 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33736
34104
|
return this.connectedClients.size;
|
|
33737
34105
|
}
|
|
33738
34106
|
// -----------------------------------------------------------------------
|
|
34107
|
+
// Diagnostic subscription API (implements DiagnosticStreamServer)
|
|
34108
|
+
//
|
|
34109
|
+
// Matches the shape of BaichuanRtspServer's diagnostic API so the
|
|
34110
|
+
// stream-diagnostic feature in the Manager app can drive either backend
|
|
34111
|
+
// with identical code.
|
|
34112
|
+
// -----------------------------------------------------------------------
|
|
34113
|
+
/**
|
|
34114
|
+
* Subscribe to the raw native stream for diagnostic purposes.
|
|
34115
|
+
* The subscriber receives the same frames the MPEG-TS muxer consumes
|
|
34116
|
+
* (pre-muxing). Counts as a "consumer" so the native stream is kept alive
|
|
34117
|
+
* for the lifetime of the subscription. If the stream is not already
|
|
34118
|
+
* running (battery camera, prestart=false), this starts it.
|
|
34119
|
+
*/
|
|
34120
|
+
async subscribeDiagnostic(id) {
|
|
34121
|
+
this.connectedClients.add(`diag:${id}`);
|
|
34122
|
+
if (!this.nativeStreamActive) {
|
|
34123
|
+
await this.startNativeStream();
|
|
34124
|
+
}
|
|
34125
|
+
if (!this.nativeFanout) {
|
|
34126
|
+
this.connectedClients.delete(`diag:${id}`);
|
|
34127
|
+
throw new Error(
|
|
34128
|
+
"Go2rtcTcpServer: native stream failed to start \u2014 cannot subscribe diagnostic"
|
|
34129
|
+
);
|
|
34130
|
+
}
|
|
34131
|
+
return this.nativeFanout.subscribe(`diag:${id}`);
|
|
34132
|
+
}
|
|
34133
|
+
/** Unsubscribe a diagnostic session and release its consumer slot. */
|
|
34134
|
+
unsubscribeDiagnostic(id) {
|
|
34135
|
+
this.removeClient(`diag:${id}`, "diagnostic unsubscribe");
|
|
34136
|
+
}
|
|
34137
|
+
/**
|
|
34138
|
+
* Returns ADTS AAC audio metadata detected from the native stream, or
|
|
34139
|
+
* null if no audio frame has been observed yet (e.g. video-only cameras
|
|
34140
|
+
* or before the first audio packet arrives).
|
|
34141
|
+
*/
|
|
34142
|
+
getAudioInfo() {
|
|
34143
|
+
return this.audioInfo;
|
|
34144
|
+
}
|
|
34145
|
+
// -----------------------------------------------------------------------
|
|
33739
34146
|
// Client handling
|
|
33740
34147
|
// -----------------------------------------------------------------------
|
|
33741
34148
|
handleClient(socket) {
|
|
@@ -33759,12 +34166,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33759
34166
|
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
33760
34167
|
);
|
|
33761
34168
|
});
|
|
33762
|
-
const cleanup = () => {
|
|
33763
|
-
this.removeClient(clientId);
|
|
34169
|
+
const cleanup = (reason) => {
|
|
34170
|
+
this.removeClient(clientId, reason);
|
|
33764
34171
|
socket.destroy();
|
|
33765
34172
|
};
|
|
33766
|
-
socket.on("error", cleanup);
|
|
33767
|
-
socket.on("close", cleanup);
|
|
34173
|
+
socket.on("error", (err) => cleanup(`error: ${err.message}`));
|
|
34174
|
+
socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
|
|
33768
34175
|
}
|
|
33769
34176
|
async feedClient(clientId, socket) {
|
|
33770
34177
|
const fanoutDeadline = Date.now() + 3e4;
|
|
@@ -33780,6 +34187,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33780
34187
|
}
|
|
33781
34188
|
if (!this.active || !this.nativeFanout) return;
|
|
33782
34189
|
const subscription = this.nativeFanout.subscribe(clientId);
|
|
34190
|
+
let muxer = null;
|
|
33783
34191
|
const prebufferSnap = this.prebuffer.slice();
|
|
33784
34192
|
let lastIdrIdx = -1;
|
|
33785
34193
|
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
@@ -33793,9 +34201,21 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33793
34201
|
this.logger.info?.(
|
|
33794
34202
|
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
33795
34203
|
);
|
|
34204
|
+
if (!muxer) {
|
|
34205
|
+
muxer = new MpegTsMuxer({
|
|
34206
|
+
videoType: this.detectedVideoType ?? "H264",
|
|
34207
|
+
includeAudio: true
|
|
34208
|
+
});
|
|
34209
|
+
}
|
|
33796
34210
|
for (const entry of replay) {
|
|
33797
34211
|
if (socket.destroyed) return;
|
|
33798
|
-
|
|
34212
|
+
let ts;
|
|
34213
|
+
if (!entry.audio) {
|
|
34214
|
+
ts = muxer.muxVideo(entry.data, entry.pts, entry.isKeyframe);
|
|
34215
|
+
} else {
|
|
34216
|
+
ts = muxer.muxAudio(entry.data, entry.pts);
|
|
34217
|
+
}
|
|
34218
|
+
if (ts.length > 0) socket.write(ts);
|
|
33799
34219
|
}
|
|
33800
34220
|
}
|
|
33801
34221
|
let seenKeyframe = lastIdrIdx >= 0;
|
|
@@ -33814,17 +34234,35 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33814
34234
|
break;
|
|
33815
34235
|
}
|
|
33816
34236
|
liveFrameCount++;
|
|
33817
|
-
|
|
34237
|
+
if (frame.audio) {
|
|
34238
|
+
if (muxer) {
|
|
34239
|
+
const pts2 = frame.microseconds ?? Date.now() * 1e3;
|
|
34240
|
+
const ts2 = muxer.muxAudio(frame.data, pts2);
|
|
34241
|
+
if (ts2.length > 0) socket.write(ts2);
|
|
34242
|
+
}
|
|
34243
|
+
continue;
|
|
34244
|
+
}
|
|
34245
|
+
const annexB = this.convertVideoFrame(frame);
|
|
33818
34246
|
if (!annexB) continue;
|
|
34247
|
+
const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
33819
34248
|
if (!seenKeyframe) {
|
|
33820
|
-
if (!
|
|
34249
|
+
if (!isKf) continue;
|
|
33821
34250
|
seenKeyframe = true;
|
|
33822
34251
|
this.logger.info?.(
|
|
33823
34252
|
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
33824
34253
|
);
|
|
34254
|
+
if (!muxer) {
|
|
34255
|
+
muxer = new MpegTsMuxer({
|
|
34256
|
+
videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
|
|
34257
|
+
includeAudio: true
|
|
34258
|
+
});
|
|
34259
|
+
}
|
|
33825
34260
|
}
|
|
33826
|
-
|
|
34261
|
+
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
34262
|
+
const ts = muxer.muxVideo(annexB, pts, isKf);
|
|
34263
|
+
socket.write(ts);
|
|
33827
34264
|
liveVideoWritten++;
|
|
34265
|
+
this.totalVideoFramesWritten++;
|
|
33828
34266
|
if (Date.now() - lastLogAt > 1e4) {
|
|
33829
34267
|
this.logger.info?.(
|
|
33830
34268
|
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
@@ -33851,14 +34289,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33851
34289
|
// Frame conversion
|
|
33852
34290
|
// -----------------------------------------------------------------------
|
|
33853
34291
|
/**
|
|
33854
|
-
* Convert a native frame to
|
|
33855
|
-
*
|
|
33856
|
-
* go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
|
|
34292
|
+
* Convert a native video frame to Annex-B.
|
|
34293
|
+
* Returns null for audio frames (handled separately by muxAudio).
|
|
33857
34294
|
*/
|
|
33858
|
-
|
|
33859
|
-
if (frame.audio)
|
|
33860
|
-
return null;
|
|
33861
|
-
}
|
|
34295
|
+
convertVideoFrame(frame) {
|
|
34296
|
+
if (frame.audio) return null;
|
|
33862
34297
|
if (frame.data.length === 0) return null;
|
|
33863
34298
|
try {
|
|
33864
34299
|
if (frame.videoType === "H264") {
|
|
@@ -33922,10 +34357,71 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33922
34357
|
return nals;
|
|
33923
34358
|
}
|
|
33924
34359
|
// -----------------------------------------------------------------------
|
|
34360
|
+
// ADTS AAC parsing (used for audio metadata exposed via getAudioInfo)
|
|
34361
|
+
// -----------------------------------------------------------------------
|
|
34362
|
+
/** True if `b` starts with an ADTS AAC syncword (0xFFF). */
|
|
34363
|
+
static isAdtsAacFrame(b) {
|
|
34364
|
+
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
34365
|
+
}
|
|
34366
|
+
/**
|
|
34367
|
+
* Parse an ADTS header into {sampleRate, channels, AudioSpecificConfig hex}.
|
|
34368
|
+
* Returns null when the buffer is not a valid ADTS frame.
|
|
34369
|
+
*/
|
|
34370
|
+
static parseAdtsSamplingInfo(b) {
|
|
34371
|
+
if (b.length < 7) return null;
|
|
34372
|
+
if (!_Go2rtcTcpServer.isAdtsAacFrame(b)) return null;
|
|
34373
|
+
const samplingIndex = b[2] >> 2 & 15;
|
|
34374
|
+
const sampleRates = [
|
|
34375
|
+
96e3,
|
|
34376
|
+
88200,
|
|
34377
|
+
64e3,
|
|
34378
|
+
48e3,
|
|
34379
|
+
44100,
|
|
34380
|
+
32e3,
|
|
34381
|
+
24e3,
|
|
34382
|
+
22050,
|
|
34383
|
+
16e3,
|
|
34384
|
+
12e3,
|
|
34385
|
+
11025,
|
|
34386
|
+
8e3,
|
|
34387
|
+
7350
|
|
34388
|
+
];
|
|
34389
|
+
const sampleRate = sampleRates[samplingIndex] ?? null;
|
|
34390
|
+
if (!sampleRate) return null;
|
|
34391
|
+
const channelConfig = (b[2] & 1) << 2 | b[3] >> 6 & 3;
|
|
34392
|
+
const channels = channelConfig === 0 ? 1 : channelConfig;
|
|
34393
|
+
const profile = b[2] >> 6 & 3;
|
|
34394
|
+
const audioObjectType = profile + 1;
|
|
34395
|
+
const asc = audioObjectType << 11 | samplingIndex << 7 | channelConfig << 3;
|
|
34396
|
+
const configHex = Buffer.from([asc >> 8 & 255, asc & 255]).toString(
|
|
34397
|
+
"hex"
|
|
34398
|
+
);
|
|
34399
|
+
return { sampleRate, channels, configHex };
|
|
34400
|
+
}
|
|
34401
|
+
// -----------------------------------------------------------------------
|
|
33925
34402
|
// Native stream management
|
|
33926
34403
|
// -----------------------------------------------------------------------
|
|
33927
34404
|
async startNativeStream() {
|
|
33928
34405
|
if (this.nativeStreamActive) return;
|
|
34406
|
+
if (!this.api.isReady) {
|
|
34407
|
+
if (this.api.isClosed) {
|
|
34408
|
+
this.logger.warn?.(
|
|
34409
|
+
`[Go2rtcTcpServer] API has been explicitly closed \u2014 stream cannot start`
|
|
34410
|
+
);
|
|
34411
|
+
return;
|
|
34412
|
+
}
|
|
34413
|
+
try {
|
|
34414
|
+
this.logger.info?.(
|
|
34415
|
+
`[Go2rtcTcpServer] API not ready (idle disconnect?), calling ensureConnected`
|
|
34416
|
+
);
|
|
34417
|
+
await this.api.ensureConnected();
|
|
34418
|
+
} catch (e) {
|
|
34419
|
+
this.logger.warn?.(
|
|
34420
|
+
`[Go2rtcTcpServer] ensureConnected failed, aborting stream start: ${e}`
|
|
34421
|
+
);
|
|
34422
|
+
return;
|
|
34423
|
+
}
|
|
34424
|
+
}
|
|
33929
34425
|
this.nativeStreamActive = true;
|
|
33930
34426
|
let dedicatedClient;
|
|
33931
34427
|
if (this.deviceId) {
|
|
@@ -33944,6 +34440,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33944
34440
|
this.logger.info?.(
|
|
33945
34441
|
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
33946
34442
|
);
|
|
34443
|
+
let hadFrames = false;
|
|
33947
34444
|
this.nativeFanout = new NativeStreamFanout2({
|
|
33948
34445
|
maxQueueItems: 200,
|
|
33949
34446
|
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
@@ -33951,17 +34448,37 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33951
34448
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
33952
34449
|
}),
|
|
33953
34450
|
onFrame: (frame) => {
|
|
34451
|
+
hadFrames = true;
|
|
34452
|
+
this.lastFrameAt = Date.now();
|
|
34453
|
+
this.totalFramesReceived++;
|
|
33954
34454
|
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
33955
34455
|
this.detectedVideoType = frame.videoType;
|
|
33956
34456
|
}
|
|
33957
|
-
|
|
33958
|
-
|
|
33959
|
-
|
|
34457
|
+
let prebufData;
|
|
34458
|
+
let isKeyframe;
|
|
34459
|
+
if (frame.audio) {
|
|
34460
|
+
if (frame.data.length === 0) return;
|
|
34461
|
+
if (!this.audioInfo) {
|
|
34462
|
+
const parsed = _Go2rtcTcpServer.parseAdtsSamplingInfo(frame.data);
|
|
34463
|
+
if (parsed) {
|
|
34464
|
+
this.audioInfo = { codec: "aac-adts", ...parsed };
|
|
34465
|
+
}
|
|
34466
|
+
}
|
|
34467
|
+
prebufData = frame.data;
|
|
34468
|
+
isKeyframe = false;
|
|
34469
|
+
} else {
|
|
34470
|
+
const annexB = this.convertVideoFrame(frame);
|
|
34471
|
+
if (!annexB || annexB.length === 0) return;
|
|
34472
|
+
prebufData = annexB;
|
|
34473
|
+
isKeyframe = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
34474
|
+
}
|
|
34475
|
+
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
33960
34476
|
this.prebuffer.push({
|
|
33961
|
-
data: Buffer.from(
|
|
34477
|
+
data: Buffer.from(prebufData),
|
|
33962
34478
|
time: Date.now(),
|
|
33963
34479
|
isKeyframe,
|
|
33964
|
-
audio: frame.audio
|
|
34480
|
+
audio: frame.audio,
|
|
34481
|
+
pts
|
|
33965
34482
|
});
|
|
33966
34483
|
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
33967
34484
|
let trimIdx = 0;
|
|
@@ -33977,23 +34494,47 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33977
34494
|
if (!this.nativeStreamActive) return;
|
|
33978
34495
|
this.nativeStreamActive = false;
|
|
33979
34496
|
this.nativeFanout = null;
|
|
34497
|
+
this.stopStreamHealthMonitor();
|
|
34498
|
+
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
34499
|
+
const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
|
|
34500
|
+
this.logger.warn?.(
|
|
34501
|
+
`[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
|
|
34502
|
+
);
|
|
33980
34503
|
if (this.dedicatedSessionRelease) {
|
|
33981
34504
|
this.dedicatedSessionRelease().catch(() => {
|
|
33982
34505
|
});
|
|
33983
34506
|
this.dedicatedSessionRelease = void 0;
|
|
33984
34507
|
}
|
|
33985
|
-
if (this.
|
|
34508
|
+
if (!this.prestartStream) {
|
|
34509
|
+
this.logger.info?.(
|
|
34510
|
+
`[Go2rtcTcpServer] battery native stream ended hadFrames=${hadFrames} channel=${this.channel} profile=${this.profile} \u2014 dropping ${this.connectedClients.size} client(s) to prevent wake loop`
|
|
34511
|
+
);
|
|
34512
|
+
for (const [, sock] of this.clientSockets) {
|
|
34513
|
+
sock.destroy();
|
|
34514
|
+
}
|
|
34515
|
+
} else if (this.active) {
|
|
34516
|
+
if (typeof this.api.isStreamProfileRejected === "function" && this.api.isStreamProfileRejected(this.channel, this.profile)) {
|
|
34517
|
+
this.logger.warn?.(
|
|
34518
|
+
`[Go2rtcTcpServer] profile rejected by device channel=${this.channel} profile=${this.profile} \u2014 not restarting`
|
|
34519
|
+
);
|
|
34520
|
+
for (const [, sock] of this.clientSockets) {
|
|
34521
|
+
sock.destroy();
|
|
34522
|
+
}
|
|
34523
|
+
return;
|
|
34524
|
+
}
|
|
33986
34525
|
this.logger.info?.(
|
|
33987
|
-
`[Go2rtcTcpServer] native stream
|
|
34526
|
+
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
33988
34527
|
);
|
|
33989
34528
|
this.startNativeStream();
|
|
33990
34529
|
}
|
|
33991
34530
|
}
|
|
33992
34531
|
});
|
|
33993
34532
|
this.nativeFanout.start();
|
|
34533
|
+
this.startStreamHealthMonitor();
|
|
33994
34534
|
}
|
|
33995
34535
|
async stopNativeStream() {
|
|
33996
34536
|
this.nativeStreamActive = false;
|
|
34537
|
+
this.stopStreamHealthMonitor();
|
|
33997
34538
|
const fanout = this.nativeFanout;
|
|
33998
34539
|
this.nativeFanout = null;
|
|
33999
34540
|
if (fanout) {
|
|
@@ -34007,14 +34548,50 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34007
34548
|
}
|
|
34008
34549
|
}
|
|
34009
34550
|
// -----------------------------------------------------------------------
|
|
34551
|
+
// Stream health monitoring
|
|
34552
|
+
// -----------------------------------------------------------------------
|
|
34553
|
+
startStreamHealthMonitor() {
|
|
34554
|
+
this.stopStreamHealthMonitor();
|
|
34555
|
+
if (this.streamTimeoutMs <= 0) return;
|
|
34556
|
+
this.lastFrameAt = Date.now();
|
|
34557
|
+
this.streamHealthTimer = setInterval(() => {
|
|
34558
|
+
if (!this.nativeStreamActive || !this.active) {
|
|
34559
|
+
this.stopStreamHealthMonitor();
|
|
34560
|
+
return;
|
|
34561
|
+
}
|
|
34562
|
+
const silenceMs = Date.now() - this.lastFrameAt;
|
|
34563
|
+
if (silenceMs > this.streamTimeoutMs) {
|
|
34564
|
+
this.logger.warn?.(
|
|
34565
|
+
`[Go2rtcTcpServer] stream inactivity timeout: no frames for ${(silenceMs / 1e3).toFixed(1)}s (threshold=${this.streamTimeoutMs}ms), totalReceived=${this.totalFramesReceived} clients=${this.connectedClients.size} \u2014 forcing stream restart`
|
|
34566
|
+
);
|
|
34567
|
+
this.stopStreamHealthMonitor();
|
|
34568
|
+
const fanout = this.nativeFanout;
|
|
34569
|
+
if (fanout) {
|
|
34570
|
+
this.nativeStreamActive = false;
|
|
34571
|
+
this.nativeFanout = null;
|
|
34572
|
+
fanout.stop().catch(() => {
|
|
34573
|
+
});
|
|
34574
|
+
}
|
|
34575
|
+
}
|
|
34576
|
+
}, Math.min(this.streamTimeoutMs / 2, 5e3));
|
|
34577
|
+
}
|
|
34578
|
+
stopStreamHealthMonitor() {
|
|
34579
|
+
if (this.streamHealthTimer) {
|
|
34580
|
+
clearInterval(this.streamHealthTimer);
|
|
34581
|
+
this.streamHealthTimer = void 0;
|
|
34582
|
+
}
|
|
34583
|
+
}
|
|
34584
|
+
// -----------------------------------------------------------------------
|
|
34010
34585
|
// Client lifecycle
|
|
34011
34586
|
// -----------------------------------------------------------------------
|
|
34012
|
-
removeClient(clientId) {
|
|
34587
|
+
removeClient(clientId, reason) {
|
|
34013
34588
|
if (!this.connectedClients.has(clientId)) return;
|
|
34014
34589
|
this.connectedClients.delete(clientId);
|
|
34015
34590
|
this.clientSockets.delete(clientId);
|
|
34591
|
+
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
34592
|
+
const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
|
|
34016
34593
|
this.logger.info?.(
|
|
34017
|
-
`[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
|
|
34594
|
+
`[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
|
|
34018
34595
|
);
|
|
34019
34596
|
this.emit("clientDisconnected", clientId);
|
|
34020
34597
|
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
@@ -36349,16 +36926,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
|
|
|
36349
36926
|
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");
|
|
36350
36927
|
}
|
|
36351
36928
|
async function pingHost(host, timeoutMs = 3e3) {
|
|
36929
|
+
const { exec } = await import("child_process");
|
|
36930
|
+
const platform2 = process.platform;
|
|
36931
|
+
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
36932
|
+
// macOS: -W is in milliseconds (Linux: seconds)
|
|
36933
|
+
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
36934
|
+
) : (
|
|
36935
|
+
// Linux/BSD-ish: -W is in seconds on most distros
|
|
36936
|
+
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
36937
|
+
);
|
|
36352
36938
|
return new Promise((resolve) => {
|
|
36353
|
-
const { exec } = require("child_process");
|
|
36354
|
-
const platform2 = process.platform;
|
|
36355
|
-
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
36356
|
-
// macOS: -W is in milliseconds (Linux: seconds)
|
|
36357
|
-
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
36358
|
-
) : (
|
|
36359
|
-
// Linux/BSD-ish: -W is in seconds on most distros
|
|
36360
|
-
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
36361
|
-
);
|
|
36362
36939
|
exec(pingCmd, (error) => {
|
|
36363
36940
|
resolve(!error);
|
|
36364
36941
|
});
|
|
@@ -36440,9 +37017,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36440
37017
|
const msg = fmtErr(e);
|
|
36441
37018
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
36442
37019
|
};
|
|
36443
|
-
const withRetries = async (label, max, op, shouldRetry) => {
|
|
37020
|
+
const withRetries = async (label, max, op, shouldRetry, isAborted) => {
|
|
36444
37021
|
let lastErr;
|
|
36445
37022
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
37023
|
+
if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
|
|
36446
37024
|
try {
|
|
36447
37025
|
if (attempt > 1) {
|
|
36448
37026
|
logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
|
|
@@ -36451,7 +37029,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36451
37029
|
} catch (e) {
|
|
36452
37030
|
lastErr = e;
|
|
36453
37031
|
const msg = fmtErr(e);
|
|
36454
|
-
const retryable = attempt < max && shouldRetry(e);
|
|
37032
|
+
const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
|
|
36455
37033
|
logger?.log?.(
|
|
36456
37034
|
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
36457
37035
|
);
|
|
@@ -36461,6 +37039,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36461
37039
|
}
|
|
36462
37040
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
|
|
36463
37041
|
};
|
|
37042
|
+
const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
|
|
37043
|
+
let raceWon = false;
|
|
37044
|
+
const methodErrors = /* @__PURE__ */ new Map();
|
|
37045
|
+
try {
|
|
37046
|
+
return await Promise.any(
|
|
37047
|
+
methods.map(async (m) => {
|
|
37048
|
+
try {
|
|
37049
|
+
const result = await loginAndDetect(m, () => raceWon);
|
|
37050
|
+
raceWon = true;
|
|
37051
|
+
return result;
|
|
37052
|
+
} catch (e) {
|
|
37053
|
+
if (!raceWon) methodErrors.set(m, fmtErr(e));
|
|
37054
|
+
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
|
|
37055
|
+
throw e;
|
|
37056
|
+
}
|
|
37057
|
+
})
|
|
37058
|
+
);
|
|
37059
|
+
} catch (e) {
|
|
37060
|
+
if (e instanceof AggregateError) {
|
|
37061
|
+
const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
|
|
37062
|
+
throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
|
|
37063
|
+
}
|
|
37064
|
+
throw e;
|
|
37065
|
+
}
|
|
37066
|
+
};
|
|
36464
37067
|
const effectiveUid = normalizeUid(uid);
|
|
36465
37068
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
36466
37069
|
const isReachable = await pingHost(host);
|
|
@@ -36490,9 +37093,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36490
37093
|
normalizedUid = normalizedDiscovered;
|
|
36491
37094
|
}
|
|
36492
37095
|
const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
36493
|
-
|
|
36494
|
-
|
|
36495
|
-
|
|
37096
|
+
return await runUdpMethodsParallel(
|
|
37097
|
+
methodsToTry,
|
|
37098
|
+
async (m, isAborted) => {
|
|
36496
37099
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
36497
37100
|
const udpApi = await withRetries(
|
|
36498
37101
|
`UDP(${m})`,
|
|
@@ -36515,11 +37118,14 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36515
37118
|
throw e;
|
|
36516
37119
|
}
|
|
36517
37120
|
},
|
|
36518
|
-
shouldRetryUdp
|
|
37121
|
+
shouldRetryUdp,
|
|
37122
|
+
isAborted
|
|
36519
37123
|
);
|
|
36520
|
-
const deviceInfo = await
|
|
36521
|
-
|
|
36522
|
-
|
|
37124
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
37125
|
+
udpApi.getInfo(),
|
|
37126
|
+
udpApi.getDeviceCapabilities(),
|
|
37127
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
37128
|
+
]);
|
|
36523
37129
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
36524
37130
|
const model = deviceInfo.type?.trim();
|
|
36525
37131
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -36558,14 +37164,8 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36558
37164
|
channelNum: 1,
|
|
36559
37165
|
api: udpApi
|
|
36560
37166
|
};
|
|
36561
|
-
}
|
|
36562
|
-
|
|
36563
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
36564
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
36565
|
-
}
|
|
36566
|
-
}
|
|
36567
|
-
throw new Error(
|
|
36568
|
-
`Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
|
|
37167
|
+
},
|
|
37168
|
+
"Forced UDP autodetect failed for all methods."
|
|
36569
37169
|
);
|
|
36570
37170
|
}
|
|
36571
37171
|
let tcpApi;
|
|
@@ -36618,54 +37218,57 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36618
37218
|
}
|
|
36619
37219
|
return void 0;
|
|
36620
37220
|
};
|
|
36621
|
-
const infoProbe = await
|
|
36622
|
-
|
|
36623
|
-
|
|
37221
|
+
const [infoProbe, supportProbe] = await Promise.all([
|
|
37222
|
+
runProbeVariants(
|
|
37223
|
+
"getInfo",
|
|
37224
|
+
[
|
|
37225
|
+
{
|
|
37226
|
+
variant: "cmd80 class=0x6414",
|
|
37227
|
+
op: () => api.getInfo(void 0, {
|
|
37228
|
+
timeoutMs: 2500,
|
|
37229
|
+
messageClass: BC_CLASS_MODERN_24
|
|
37230
|
+
})
|
|
37231
|
+
},
|
|
37232
|
+
{
|
|
37233
|
+
variant: "cmd80 class=0x6614",
|
|
37234
|
+
op: () => api.getInfo(void 0, {
|
|
37235
|
+
timeoutMs: 3e3,
|
|
37236
|
+
messageClass: BC_CLASS_MODERN_20
|
|
37237
|
+
})
|
|
37238
|
+
},
|
|
37239
|
+
{
|
|
37240
|
+
variant: "cmd318(ch0) class=0x6414",
|
|
37241
|
+
op: () => api.getInfo(0, {
|
|
37242
|
+
timeoutMs: 3e3,
|
|
37243
|
+
messageClass: BC_CLASS_MODERN_24
|
|
37244
|
+
})
|
|
37245
|
+
},
|
|
37246
|
+
{
|
|
37247
|
+
variant: "cmd318(ch0) class=0x6614",
|
|
37248
|
+
op: () => api.getInfo(0, {
|
|
37249
|
+
timeoutMs: 3500,
|
|
37250
|
+
messageClass: BC_CLASS_MODERN_20
|
|
37251
|
+
})
|
|
37252
|
+
}
|
|
37253
|
+
]
|
|
37254
|
+
),
|
|
37255
|
+
// Support probes (cmd 199). Some firmwares may not support it or are slow.
|
|
37256
|
+
runProbeVariants("getSupportInfo", [
|
|
36624
37257
|
{
|
|
36625
|
-
variant: "
|
|
36626
|
-
op: () => api.
|
|
37258
|
+
variant: "cmd199 class=0x6414",
|
|
37259
|
+
op: () => api.getSupportInfo({
|
|
36627
37260
|
timeoutMs: 2500,
|
|
36628
37261
|
messageClass: BC_CLASS_MODERN_24
|
|
36629
37262
|
})
|
|
36630
37263
|
},
|
|
36631
37264
|
{
|
|
36632
|
-
variant: "
|
|
36633
|
-
op: () => api.
|
|
36634
|
-
timeoutMs: 3e3,
|
|
36635
|
-
messageClass: BC_CLASS_MODERN_20
|
|
36636
|
-
})
|
|
36637
|
-
},
|
|
36638
|
-
{
|
|
36639
|
-
variant: "cmd318(ch0) class=0x6414",
|
|
36640
|
-
op: () => api.getInfo(0, {
|
|
36641
|
-
timeoutMs: 3e3,
|
|
36642
|
-
messageClass: BC_CLASS_MODERN_24
|
|
36643
|
-
})
|
|
36644
|
-
},
|
|
36645
|
-
{
|
|
36646
|
-
variant: "cmd318(ch0) class=0x6614",
|
|
36647
|
-
op: () => api.getInfo(0, {
|
|
37265
|
+
variant: "cmd199 class=0x6614",
|
|
37266
|
+
op: () => api.getSupportInfo({
|
|
36648
37267
|
timeoutMs: 3500,
|
|
36649
37268
|
messageClass: BC_CLASS_MODERN_20
|
|
36650
37269
|
})
|
|
36651
37270
|
}
|
|
36652
|
-
]
|
|
36653
|
-
);
|
|
36654
|
-
const supportProbe = await runProbeVariants("getSupportInfo", [
|
|
36655
|
-
{
|
|
36656
|
-
variant: "cmd199 class=0x6414",
|
|
36657
|
-
op: () => api.getSupportInfo({
|
|
36658
|
-
timeoutMs: 2500,
|
|
36659
|
-
messageClass: BC_CLASS_MODERN_24
|
|
36660
|
-
})
|
|
36661
|
-
},
|
|
36662
|
-
{
|
|
36663
|
-
variant: "cmd199 class=0x6614",
|
|
36664
|
-
op: () => api.getSupportInfo({
|
|
36665
|
-
timeoutMs: 3500,
|
|
36666
|
-
messageClass: BC_CLASS_MODERN_20
|
|
36667
|
-
})
|
|
36668
|
-
}
|
|
37271
|
+
])
|
|
36669
37272
|
]);
|
|
36670
37273
|
const deviceInfo = infoProbe?.value;
|
|
36671
37274
|
const support = supportProbe?.value;
|
|
@@ -36757,9 +37360,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36757
37360
|
}
|
|
36758
37361
|
try {
|
|
36759
37362
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
36760
|
-
const deviceInfo = await
|
|
36761
|
-
|
|
36762
|
-
|
|
37363
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
37364
|
+
udpApi.getInfo(),
|
|
37365
|
+
udpApi.getDeviceCapabilities(),
|
|
37366
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
37367
|
+
]);
|
|
36763
37368
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
36764
37369
|
const model = deviceInfo.type?.trim();
|
|
36765
37370
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -36803,21 +37408,17 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36803
37408
|
};
|
|
36804
37409
|
};
|
|
36805
37410
|
const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
36806
|
-
const
|
|
36807
|
-
|
|
36808
|
-
|
|
37411
|
+
const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
|
|
37412
|
+
return await runUdpMethodsParallel(
|
|
37413
|
+
viableMethods,
|
|
37414
|
+
async (m, isAborted) => {
|
|
36809
37415
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
36810
37416
|
const udpApi = await withRetries(
|
|
36811
37417
|
`UDP(${m})`,
|
|
36812
37418
|
maxRetries,
|
|
36813
37419
|
async (attempt) => {
|
|
36814
|
-
const apiInputs = {
|
|
36815
|
-
|
|
36816
|
-
udpDiscoveryMethod: m
|
|
36817
|
-
};
|
|
36818
|
-
if (normalizedUid) {
|
|
36819
|
-
apiInputs.uid = normalizedUid;
|
|
36820
|
-
}
|
|
37420
|
+
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
37421
|
+
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
36821
37422
|
const api = createBaichuanApi(apiInputs, "udp");
|
|
36822
37423
|
try {
|
|
36823
37424
|
await api.login();
|
|
@@ -36832,20 +37433,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36832
37433
|
throw e;
|
|
36833
37434
|
}
|
|
36834
37435
|
},
|
|
36835
|
-
shouldRetryUdp
|
|
37436
|
+
shouldRetryUdp,
|
|
37437
|
+
isAborted
|
|
36836
37438
|
);
|
|
36837
|
-
return
|
|
36838
|
-
}
|
|
36839
|
-
|
|
36840
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
36841
|
-
try {
|
|
36842
|
-
} catch {
|
|
36843
|
-
}
|
|
36844
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
36845
|
-
}
|
|
36846
|
-
}
|
|
36847
|
-
throw new Error(
|
|
36848
|
-
`UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
|
|
37439
|
+
return detectOverUdpApi(udpApi, m);
|
|
37440
|
+
},
|
|
37441
|
+
"UDP discovery failed for all methods."
|
|
36849
37442
|
);
|
|
36850
37443
|
} catch (udpError) {
|
|
36851
37444
|
logger?.log?.(
|
|
@@ -37210,6 +37803,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
37210
37803
|
HlsSessionManager,
|
|
37211
37804
|
Intercom,
|
|
37212
37805
|
MjpegTransformer,
|
|
37806
|
+
MpegTsMuxer,
|
|
37213
37807
|
NVR_HUB_EXACT_TYPES,
|
|
37214
37808
|
NVR_HUB_MODEL_PATTERNS,
|
|
37215
37809
|
ReolinkBaichuanApi,
|
|
@@ -37268,6 +37862,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
37268
37862
|
createRfc4571TcpServerForReplay,
|
|
37269
37863
|
createRtspProxyServer,
|
|
37270
37864
|
createTaggedLogger,
|
|
37865
|
+
decideSleepInferenceTransition,
|
|
37271
37866
|
decideVideoclipTranscodeMode,
|
|
37272
37867
|
decodeHeader,
|
|
37273
37868
|
deriveAesKey,
|