@apocaliss92/nodelink-js 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DiagnosticsTools-UMN4C7SY.js → DiagnosticsTools-HJDH4GPP.js} +2 -2
- package/dist/{chunk-GKLOJJ34.js → chunk-VBYF3BQX.js} +639 -341
- package/dist/chunk-VBYF3BQX.js.map +1 -0
- package/dist/{chunk-TR3V5FTO.js → chunk-YKKQDUKU.js} +3 -3
- package/dist/{chunk-TR3V5FTO.js.map → chunk-YKKQDUKU.js.map} +1 -1
- package/dist/cli/rtsp-server.cjs +607 -311
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +793 -328
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +254 -6
- package/dist/index.d.ts +238 -5
- package/dist/index.js +188 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GKLOJJ34.js.map +0 -1
- /package/dist/{DiagnosticsTools-UMN4C7SY.js.map → DiagnosticsTools-HJDH4GPP.js.map} +0 -0
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.
|
|
15827
|
+
this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
15828
|
+
this.includeAudio = options.includeAudio ?? true;
|
|
15643
15829
|
}
|
|
15644
15830
|
/**
|
|
15645
|
-
*
|
|
15646
|
-
|
|
15647
|
-
static resetCounters() {
|
|
15648
|
-
patCc = 0;
|
|
15649
|
-
pmtCc = 0;
|
|
15650
|
-
videoCc = 0;
|
|
15651
|
-
}
|
|
15652
|
-
/**
|
|
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) => {
|
|
@@ -18379,7 +18632,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18379
18632
|
statePollingInterval;
|
|
18380
18633
|
udpSleepInferenceInterval;
|
|
18381
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();
|
|
18382
18643
|
udpSleepInferenceIntervalMs = 2e3;
|
|
18644
|
+
/** Consecutive inference polls required to commit a new sleeping/awake state. */
|
|
18645
|
+
udpSleepInferenceHysteresisPolls = 2;
|
|
18383
18646
|
lastMotionState;
|
|
18384
18647
|
lastAiState;
|
|
18385
18648
|
aiStatePollingDisabled = false;
|
|
@@ -18846,6 +19109,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18846
19109
|
*/
|
|
18847
19110
|
attachD2cDiscListener(client) {
|
|
18848
19111
|
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
19112
|
+
client.on("error", () => {
|
|
19113
|
+
});
|
|
18849
19114
|
}
|
|
18850
19115
|
/**
|
|
18851
19116
|
* Acquire a socket from the pool by tag.
|
|
@@ -18964,6 +19229,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18964
19229
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18965
19230
|
const newClient = new BaichuanClient(clientOpts);
|
|
18966
19231
|
this.attachD2cDiscListener(newClient);
|
|
19232
|
+
newClient.on("error", (err) => {
|
|
19233
|
+
log?.debug?.(
|
|
19234
|
+
`[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
|
|
19235
|
+
);
|
|
19236
|
+
});
|
|
18967
19237
|
await newClient.login();
|
|
18968
19238
|
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18969
19239
|
if (existingCooldown) {
|
|
@@ -19479,6 +19749,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19479
19749
|
* Only counts sessions from our own IP address.
|
|
19480
19750
|
*/
|
|
19481
19751
|
async maybeRebootOnTooManySessions() {
|
|
19752
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
19482
19753
|
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
19483
19754
|
if (this.sessionGuardRebootInFlight) return;
|
|
19484
19755
|
const cooldownMs = 10 * 6e4;
|
|
@@ -19920,6 +20191,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19920
20191
|
}
|
|
19921
20192
|
async renewSimpleEventSubscription() {
|
|
19922
20193
|
if (this.simpleEventListeners.size === 0) return;
|
|
20194
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
19923
20195
|
if (this.simpleEventResubscribeInFlight)
|
|
19924
20196
|
return await this.simpleEventResubscribeInFlight;
|
|
19925
20197
|
this.simpleEventResubscribeInFlight = (async () => {
|
|
@@ -23687,23 +23959,32 @@ ${stderr}`)
|
|
|
23687
23959
|
return;
|
|
23688
23960
|
}
|
|
23689
23961
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
23962
|
+
if (!this.client.isSocketConnected?.()) {
|
|
23963
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23964
|
+
return;
|
|
23965
|
+
}
|
|
23690
23966
|
const status = this.getSleepStatus({ channel });
|
|
23691
23967
|
if (status.state === "unknown") return;
|
|
23692
|
-
const
|
|
23693
|
-
this.
|
|
23694
|
-
|
|
23695
|
-
|
|
23696
|
-
|
|
23697
|
-
|
|
23698
|
-
|
|
23699
|
-
|
|
23700
|
-
|
|
23701
|
-
|
|
23702
|
-
|
|
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);
|
|
23703
23984
|
}
|
|
23704
|
-
if (
|
|
23985
|
+
if (decision.emit) {
|
|
23705
23986
|
this.dispatchSimpleEvent({
|
|
23706
|
-
type:
|
|
23987
|
+
type: decision.emit,
|
|
23707
23988
|
channel,
|
|
23708
23989
|
timestamp: Date.now()
|
|
23709
23990
|
});
|
|
@@ -23726,6 +24007,7 @@ ${stderr}`)
|
|
|
23726
24007
|
this.udpSleepInferenceInterval = void 0;
|
|
23727
24008
|
}
|
|
23728
24009
|
this.udpLastInferredSleepStateByChannel.clear();
|
|
24010
|
+
this.udpPendingSleepStateByChannel.clear();
|
|
23729
24011
|
}
|
|
23730
24012
|
/**
|
|
23731
24013
|
* GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
|
|
@@ -27324,8 +27606,8 @@ ${scheduleItems}
|
|
|
27324
27606
|
);
|
|
27325
27607
|
let args;
|
|
27326
27608
|
if (useMpegTsMuxer) {
|
|
27327
|
-
MpegTsMuxer
|
|
27328
|
-
tsMuxer
|
|
27609
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
27610
|
+
tsMuxer.reset();
|
|
27329
27611
|
args = [
|
|
27330
27612
|
"-hide_banner",
|
|
27331
27613
|
"-loglevel",
|
|
@@ -27489,7 +27771,7 @@ ${scheduleItems}
|
|
|
27489
27771
|
startFfmpeg(videoType);
|
|
27490
27772
|
frameCount++;
|
|
27491
27773
|
if (useMpegTsMuxer && tsMuxer) {
|
|
27492
|
-
const tsData = tsMuxer.
|
|
27774
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
27493
27775
|
input.write(tsData);
|
|
27494
27776
|
} else {
|
|
27495
27777
|
if (videoType === "H264") input.write(H264_AUD);
|
|
@@ -27778,8 +28060,8 @@ ${scheduleItems}
|
|
|
27778
28060
|
logger?.log?.(
|
|
27779
28061
|
`[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
|
|
27780
28062
|
);
|
|
27781
|
-
MpegTsMuxer
|
|
27782
|
-
tsMuxer
|
|
28063
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
28064
|
+
tsMuxer.reset();
|
|
27783
28065
|
const args = [
|
|
27784
28066
|
"-hide_banner",
|
|
27785
28067
|
"-loglevel",
|
|
@@ -27944,7 +28226,7 @@ ${scheduleItems}
|
|
|
27944
28226
|
startFfmpeg(videoType);
|
|
27945
28227
|
frameCount++;
|
|
27946
28228
|
if (tsMuxer) {
|
|
27947
|
-
const tsData = tsMuxer.
|
|
28229
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
27948
28230
|
input.write(tsData);
|
|
27949
28231
|
}
|
|
27950
28232
|
if (frameCount === 1) {
|
|
@@ -33736,6 +34018,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33736
34018
|
totalVideoFramesWritten = 0;
|
|
33737
34019
|
// Prebuffer
|
|
33738
34020
|
prebuffer = [];
|
|
34021
|
+
// Audio metadata — populated on first valid ADTS AAC frame.
|
|
34022
|
+
// Exposed via getAudioInfo() for the stream-diagnostics feature.
|
|
34023
|
+
audioInfo = null;
|
|
33739
34024
|
constructor(options) {
|
|
33740
34025
|
super();
|
|
33741
34026
|
this.api = options.api;
|
|
@@ -33819,6 +34104,45 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33819
34104
|
return this.connectedClients.size;
|
|
33820
34105
|
}
|
|
33821
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
|
+
// -----------------------------------------------------------------------
|
|
33822
34146
|
// Client handling
|
|
33823
34147
|
// -----------------------------------------------------------------------
|
|
33824
34148
|
handleClient(socket) {
|
|
@@ -33863,6 +34187,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33863
34187
|
}
|
|
33864
34188
|
if (!this.active || !this.nativeFanout) return;
|
|
33865
34189
|
const subscription = this.nativeFanout.subscribe(clientId);
|
|
34190
|
+
let muxer = null;
|
|
33866
34191
|
const prebufferSnap = this.prebuffer.slice();
|
|
33867
34192
|
let lastIdrIdx = -1;
|
|
33868
34193
|
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
@@ -33876,9 +34201,21 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33876
34201
|
this.logger.info?.(
|
|
33877
34202
|
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
33878
34203
|
);
|
|
34204
|
+
if (!muxer) {
|
|
34205
|
+
muxer = new MpegTsMuxer({
|
|
34206
|
+
videoType: this.detectedVideoType ?? "H264",
|
|
34207
|
+
includeAudio: true
|
|
34208
|
+
});
|
|
34209
|
+
}
|
|
33879
34210
|
for (const entry of replay) {
|
|
33880
34211
|
if (socket.destroyed) return;
|
|
33881
|
-
|
|
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);
|
|
33882
34219
|
}
|
|
33883
34220
|
}
|
|
33884
34221
|
let seenKeyframe = lastIdrIdx >= 0;
|
|
@@ -33897,16 +34234,33 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33897
34234
|
break;
|
|
33898
34235
|
}
|
|
33899
34236
|
liveFrameCount++;
|
|
33900
|
-
|
|
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);
|
|
33901
34246
|
if (!annexB) continue;
|
|
34247
|
+
const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
33902
34248
|
if (!seenKeyframe) {
|
|
33903
|
-
if (!
|
|
34249
|
+
if (!isKf) continue;
|
|
33904
34250
|
seenKeyframe = true;
|
|
33905
34251
|
this.logger.info?.(
|
|
33906
34252
|
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
33907
34253
|
);
|
|
34254
|
+
if (!muxer) {
|
|
34255
|
+
muxer = new MpegTsMuxer({
|
|
34256
|
+
videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
|
|
34257
|
+
includeAudio: true
|
|
34258
|
+
});
|
|
34259
|
+
}
|
|
33908
34260
|
}
|
|
33909
|
-
|
|
34261
|
+
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
34262
|
+
const ts = muxer.muxVideo(annexB, pts, isKf);
|
|
34263
|
+
socket.write(ts);
|
|
33910
34264
|
liveVideoWritten++;
|
|
33911
34265
|
this.totalVideoFramesWritten++;
|
|
33912
34266
|
if (Date.now() - lastLogAt > 1e4) {
|
|
@@ -33935,14 +34289,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33935
34289
|
// Frame conversion
|
|
33936
34290
|
// -----------------------------------------------------------------------
|
|
33937
34291
|
/**
|
|
33938
|
-
* Convert a native frame to
|
|
33939
|
-
*
|
|
33940
|
-
* 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).
|
|
33941
34294
|
*/
|
|
33942
|
-
|
|
33943
|
-
if (frame.audio)
|
|
33944
|
-
return null;
|
|
33945
|
-
}
|
|
34295
|
+
convertVideoFrame(frame) {
|
|
34296
|
+
if (frame.audio) return null;
|
|
33946
34297
|
if (frame.data.length === 0) return null;
|
|
33947
34298
|
try {
|
|
33948
34299
|
if (frame.videoType === "H264") {
|
|
@@ -34006,10 +34357,71 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34006
34357
|
return nals;
|
|
34007
34358
|
}
|
|
34008
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
|
+
// -----------------------------------------------------------------------
|
|
34009
34402
|
// Native stream management
|
|
34010
34403
|
// -----------------------------------------------------------------------
|
|
34011
34404
|
async startNativeStream() {
|
|
34012
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
|
+
}
|
|
34013
34425
|
this.nativeStreamActive = true;
|
|
34014
34426
|
let dedicatedClient;
|
|
34015
34427
|
if (this.deviceId) {
|
|
@@ -34028,6 +34440,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34028
34440
|
this.logger.info?.(
|
|
34029
34441
|
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
34030
34442
|
);
|
|
34443
|
+
let hadFrames = false;
|
|
34031
34444
|
this.nativeFanout = new NativeStreamFanout2({
|
|
34032
34445
|
maxQueueItems: 200,
|
|
34033
34446
|
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
@@ -34035,19 +34448,37 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34035
34448
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
34036
34449
|
}),
|
|
34037
34450
|
onFrame: (frame) => {
|
|
34451
|
+
hadFrames = true;
|
|
34038
34452
|
this.lastFrameAt = Date.now();
|
|
34039
34453
|
this.totalFramesReceived++;
|
|
34040
34454
|
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
34041
34455
|
this.detectedVideoType = frame.videoType;
|
|
34042
34456
|
}
|
|
34043
|
-
|
|
34044
|
-
|
|
34045
|
-
|
|
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;
|
|
34046
34476
|
this.prebuffer.push({
|
|
34047
|
-
data: Buffer.from(
|
|
34477
|
+
data: Buffer.from(prebufData),
|
|
34048
34478
|
time: Date.now(),
|
|
34049
34479
|
isKeyframe,
|
|
34050
|
-
audio: frame.audio
|
|
34480
|
+
audio: frame.audio,
|
|
34481
|
+
pts
|
|
34051
34482
|
});
|
|
34052
34483
|
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
34053
34484
|
let trimIdx = 0;
|
|
@@ -34074,7 +34505,23 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34074
34505
|
});
|
|
34075
34506
|
this.dedicatedSessionRelease = void 0;
|
|
34076
34507
|
}
|
|
34077
|
-
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
|
+
}
|
|
34078
34525
|
this.logger.info?.(
|
|
34079
34526
|
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
34080
34527
|
);
|
|
@@ -36570,9 +37017,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36570
37017
|
const msg = fmtErr(e);
|
|
36571
37018
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
36572
37019
|
};
|
|
36573
|
-
const withRetries = async (label, max, op, shouldRetry) => {
|
|
37020
|
+
const withRetries = async (label, max, op, shouldRetry, isAborted) => {
|
|
36574
37021
|
let lastErr;
|
|
36575
37022
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
37023
|
+
if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
|
|
36576
37024
|
try {
|
|
36577
37025
|
if (attempt > 1) {
|
|
36578
37026
|
logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
|
|
@@ -36581,7 +37029,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36581
37029
|
} catch (e) {
|
|
36582
37030
|
lastErr = e;
|
|
36583
37031
|
const msg = fmtErr(e);
|
|
36584
|
-
const retryable = attempt < max && shouldRetry(e);
|
|
37032
|
+
const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
|
|
36585
37033
|
logger?.log?.(
|
|
36586
37034
|
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
36587
37035
|
);
|
|
@@ -36591,6 +37039,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36591
37039
|
}
|
|
36592
37040
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
|
|
36593
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
|
+
};
|
|
36594
37067
|
const effectiveUid = normalizeUid(uid);
|
|
36595
37068
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
36596
37069
|
const isReachable = await pingHost(host);
|
|
@@ -36620,9 +37093,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36620
37093
|
normalizedUid = normalizedDiscovered;
|
|
36621
37094
|
}
|
|
36622
37095
|
const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
36623
|
-
|
|
36624
|
-
|
|
36625
|
-
|
|
37096
|
+
return await runUdpMethodsParallel(
|
|
37097
|
+
methodsToTry,
|
|
37098
|
+
async (m, isAborted) => {
|
|
36626
37099
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
36627
37100
|
const udpApi = await withRetries(
|
|
36628
37101
|
`UDP(${m})`,
|
|
@@ -36645,11 +37118,14 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36645
37118
|
throw e;
|
|
36646
37119
|
}
|
|
36647
37120
|
},
|
|
36648
|
-
shouldRetryUdp
|
|
37121
|
+
shouldRetryUdp,
|
|
37122
|
+
isAborted
|
|
36649
37123
|
);
|
|
36650
|
-
const deviceInfo = await
|
|
36651
|
-
|
|
36652
|
-
|
|
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
|
+
]);
|
|
36653
37129
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
36654
37130
|
const model = deviceInfo.type?.trim();
|
|
36655
37131
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -36688,14 +37164,8 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36688
37164
|
channelNum: 1,
|
|
36689
37165
|
api: udpApi
|
|
36690
37166
|
};
|
|
36691
|
-
}
|
|
36692
|
-
|
|
36693
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
36694
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
36695
|
-
}
|
|
36696
|
-
}
|
|
36697
|
-
throw new Error(
|
|
36698
|
-
`Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
|
|
37167
|
+
},
|
|
37168
|
+
"Forced UDP autodetect failed for all methods."
|
|
36699
37169
|
);
|
|
36700
37170
|
}
|
|
36701
37171
|
let tcpApi;
|
|
@@ -36748,54 +37218,57 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36748
37218
|
}
|
|
36749
37219
|
return void 0;
|
|
36750
37220
|
};
|
|
36751
|
-
const infoProbe = await
|
|
36752
|
-
|
|
36753
|
-
|
|
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", [
|
|
36754
37257
|
{
|
|
36755
|
-
variant: "
|
|
36756
|
-
op: () => api.
|
|
37258
|
+
variant: "cmd199 class=0x6414",
|
|
37259
|
+
op: () => api.getSupportInfo({
|
|
36757
37260
|
timeoutMs: 2500,
|
|
36758
37261
|
messageClass: BC_CLASS_MODERN_24
|
|
36759
37262
|
})
|
|
36760
37263
|
},
|
|
36761
37264
|
{
|
|
36762
|
-
variant: "
|
|
36763
|
-
op: () => api.
|
|
36764
|
-
timeoutMs: 3e3,
|
|
36765
|
-
messageClass: BC_CLASS_MODERN_20
|
|
36766
|
-
})
|
|
36767
|
-
},
|
|
36768
|
-
{
|
|
36769
|
-
variant: "cmd318(ch0) class=0x6414",
|
|
36770
|
-
op: () => api.getInfo(0, {
|
|
36771
|
-
timeoutMs: 3e3,
|
|
36772
|
-
messageClass: BC_CLASS_MODERN_24
|
|
36773
|
-
})
|
|
36774
|
-
},
|
|
36775
|
-
{
|
|
36776
|
-
variant: "cmd318(ch0) class=0x6614",
|
|
36777
|
-
op: () => api.getInfo(0, {
|
|
37265
|
+
variant: "cmd199 class=0x6614",
|
|
37266
|
+
op: () => api.getSupportInfo({
|
|
36778
37267
|
timeoutMs: 3500,
|
|
36779
37268
|
messageClass: BC_CLASS_MODERN_20
|
|
36780
37269
|
})
|
|
36781
37270
|
}
|
|
36782
|
-
]
|
|
36783
|
-
);
|
|
36784
|
-
const supportProbe = await runProbeVariants("getSupportInfo", [
|
|
36785
|
-
{
|
|
36786
|
-
variant: "cmd199 class=0x6414",
|
|
36787
|
-
op: () => api.getSupportInfo({
|
|
36788
|
-
timeoutMs: 2500,
|
|
36789
|
-
messageClass: BC_CLASS_MODERN_24
|
|
36790
|
-
})
|
|
36791
|
-
},
|
|
36792
|
-
{
|
|
36793
|
-
variant: "cmd199 class=0x6614",
|
|
36794
|
-
op: () => api.getSupportInfo({
|
|
36795
|
-
timeoutMs: 3500,
|
|
36796
|
-
messageClass: BC_CLASS_MODERN_20
|
|
36797
|
-
})
|
|
36798
|
-
}
|
|
37271
|
+
])
|
|
36799
37272
|
]);
|
|
36800
37273
|
const deviceInfo = infoProbe?.value;
|
|
36801
37274
|
const support = supportProbe?.value;
|
|
@@ -36887,9 +37360,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36887
37360
|
}
|
|
36888
37361
|
try {
|
|
36889
37362
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
36890
|
-
const deviceInfo = await
|
|
36891
|
-
|
|
36892
|
-
|
|
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
|
+
]);
|
|
36893
37368
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
36894
37369
|
const model = deviceInfo.type?.trim();
|
|
36895
37370
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -36933,21 +37408,17 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36933
37408
|
};
|
|
36934
37409
|
};
|
|
36935
37410
|
const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
36936
|
-
const
|
|
36937
|
-
|
|
36938
|
-
|
|
37411
|
+
const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
|
|
37412
|
+
return await runUdpMethodsParallel(
|
|
37413
|
+
viableMethods,
|
|
37414
|
+
async (m, isAborted) => {
|
|
36939
37415
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
36940
37416
|
const udpApi = await withRetries(
|
|
36941
37417
|
`UDP(${m})`,
|
|
36942
37418
|
maxRetries,
|
|
36943
37419
|
async (attempt) => {
|
|
36944
|
-
const apiInputs = {
|
|
36945
|
-
|
|
36946
|
-
udpDiscoveryMethod: m
|
|
36947
|
-
};
|
|
36948
|
-
if (normalizedUid) {
|
|
36949
|
-
apiInputs.uid = normalizedUid;
|
|
36950
|
-
}
|
|
37420
|
+
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
37421
|
+
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
36951
37422
|
const api = createBaichuanApi(apiInputs, "udp");
|
|
36952
37423
|
try {
|
|
36953
37424
|
await api.login();
|
|
@@ -36962,20 +37433,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
36962
37433
|
throw e;
|
|
36963
37434
|
}
|
|
36964
37435
|
},
|
|
36965
|
-
shouldRetryUdp
|
|
37436
|
+
shouldRetryUdp,
|
|
37437
|
+
isAborted
|
|
36966
37438
|
);
|
|
36967
|
-
return
|
|
36968
|
-
}
|
|
36969
|
-
|
|
36970
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
36971
|
-
try {
|
|
36972
|
-
} catch {
|
|
36973
|
-
}
|
|
36974
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
36975
|
-
}
|
|
36976
|
-
}
|
|
36977
|
-
throw new Error(
|
|
36978
|
-
`UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
|
|
37439
|
+
return detectOverUdpApi(udpApi, m);
|
|
37440
|
+
},
|
|
37441
|
+
"UDP discovery failed for all methods."
|
|
36979
37442
|
);
|
|
36980
37443
|
} catch (udpError) {
|
|
36981
37444
|
logger?.log?.(
|
|
@@ -37340,6 +37803,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
37340
37803
|
HlsSessionManager,
|
|
37341
37804
|
Intercom,
|
|
37342
37805
|
MjpegTransformer,
|
|
37806
|
+
MpegTsMuxer,
|
|
37343
37807
|
NVR_HUB_EXACT_TYPES,
|
|
37344
37808
|
NVR_HUB_MODEL_PATTERNS,
|
|
37345
37809
|
ReolinkBaichuanApi,
|
|
@@ -37398,6 +37862,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
37398
37862
|
createRfc4571TcpServerForReplay,
|
|
37399
37863
|
createRtspProxyServer,
|
|
37400
37864
|
createTaggedLogger,
|
|
37865
|
+
decideSleepInferenceTransition,
|
|
37401
37866
|
decideVideoclipTranscodeMode,
|
|
37402
37867
|
decodeHeader,
|
|
37403
37868
|
deriveAesKey,
|