@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/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
- while (!closed) {
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 = "BaichuanRtspServer";
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
- const dbg = this.api.client.getDebugConfig();
13527
- return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
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.authCredentials = options.credentials ?? [];
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}:${realm}:${cred.password}`);
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
- try {
13792
- const metadata = await this.api.getStreamMetadata(this.channel);
13793
- const stream = metadata.streams.find((s) => s.profile === this.profile);
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
- this.streamMetadata = { frameRate: 25 };
13809
- this.setFlowVideoType("H264", "metadata unavailable");
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://${this.listenHost}:${this.listenPort}${this.path}/`
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
- await this.startNativeStream();
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
- void processBuffer();
14351
+ runProcessBuffer();
14224
14352
  });
14225
14353
  if (buffer.includes("\r\n\r\n")) {
14226
- void processBuffer();
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 && this.api.client.getTransport() === "tcp" && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
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 PAT_PID = 0;
15478
- var PMT_PID = 4096;
15479
- var VIDEO_PID = 256;
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 patCc = 0;
15483
- var pmtCc = 0;
15484
- var videoCc = 0;
15485
- function createPat() {
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
- streamType;
15636
- patSent = false;
15637
- pmtSent = false;
15638
- patPmtInterval = 0;
15639
- patPmtIntervalMax = 40;
15640
- // Send PAT/PMT every ~40 frames
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.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
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 data - Annex-B video data (with start codes)
15656
- * @param microseconds - Frame timestamp in microseconds
15657
- * @param isKeyframe - Whether this is a keyframe
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
- mux(data, microseconds, isKeyframe) {
15661
- const packets = [];
15662
- if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
15663
- packets.push(createPat());
15664
- packets.push(createPmt(this.streamType));
15665
- this.patSent = true;
15666
- this.pmtSent = true;
15667
- this.patPmtInterval = 0;
15668
- }
15669
- this.patPmtInterval++;
15670
- const videoPackets = createVideoPes(data, microseconds, isKeyframe);
15671
- packets.push(...videoPackets);
15672
- return Buffer.concat(packets);
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 !== void 0 || battery.chargeStatus !== void 0 || battery.adapterStatus !== void 0) {
18230
- this.dispatchSimpleEvent({
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 prev = this.udpLastInferredSleepStateByChannel.get(channel);
23672
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
23673
- if (prev === void 0) {
23674
- if (status.state === "sleeping") {
23675
- this.dispatchSimpleEvent({
23676
- type: "sleeping",
23677
- channel,
23678
- timestamp: Date.now()
23679
- });
23680
- }
23681
- return;
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 (prev !== status.state) {
23985
+ if (decision.emit) {
23684
23986
  this.dispatchSimpleEvent({
23685
- type: status.state === "sleeping" ? "sleeping" : "awake",
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.resetCounters();
27253
- tsMuxer = new MpegTsMuxer({ videoType });
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.mux(data, microseconds, isKeyframe);
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.resetCounters();
27707
- tsMuxer = new MpegTsMuxer({ videoType });
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.mux(data, microseconds, isKeyframe);
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
- socket.write(entry.data);
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
- const annexB = this.convertFrame(frame);
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 (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
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
- socket.write(annexB);
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 wire-ready Annex-B.
33855
- * Audio frames are skipped raw TCP carries only video (Annex-B).
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
- convertFrame(frame) {
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
- const wireData = this.convertFrame(frame);
33958
- if (!wireData || wireData.length === 0) return;
33959
- const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
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(wireData),
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.active && (this.connectedClients.size > 0 || this.prestartStream)) {
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 ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
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
- const udpErrors = [];
36494
- for (const m of methodsToTry) {
36495
- try {
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 udpApi.getInfo();
36521
- const capabilities = await udpApi.getDeviceCapabilities();
36522
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
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
- } catch (e) {
36562
- const msg = fmtErr(e);
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 runProbeVariants(
36622
- "getInfo",
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: "cmd80 class=0x6414",
36626
- op: () => api.getInfo(void 0, {
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: "cmd80 class=0x6614",
36633
- op: () => api.getInfo(void 0, {
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 udpApi.getInfo();
36761
- const capabilities = await udpApi.getDeviceCapabilities();
36762
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
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 udpErrors = [];
36807
- for (const m of methodsToTry) {
36808
- try {
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
- ...inputs,
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 await detectOverUdpApi(udpApi, m);
36838
- } catch (e) {
36839
- const msg = e?.message || e?.toString?.() || String(e);
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,