@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/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;
15827
+ this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
15828
+ this.includeAudio = options.includeAudio ?? true;
15643
15829
  }
15644
15830
  /**
15645
- * Reset continuity counters (call when starting a new stream).
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 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) => {
@@ -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 prev = this.udpLastInferredSleepStateByChannel.get(channel);
23693
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
23694
- if (prev === void 0) {
23695
- if (status.state === "sleeping") {
23696
- this.dispatchSimpleEvent({
23697
- type: "sleeping",
23698
- channel,
23699
- timestamp: Date.now()
23700
- });
23701
- }
23702
- 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);
23703
23984
  }
23704
- if (prev !== status.state) {
23985
+ if (decision.emit) {
23705
23986
  this.dispatchSimpleEvent({
23706
- type: status.state === "sleeping" ? "sleeping" : "awake",
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.resetCounters();
27328
- tsMuxer = new MpegTsMuxer({ videoType });
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.mux(data, microseconds, isKeyframe);
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.resetCounters();
27782
- tsMuxer = new MpegTsMuxer({ videoType });
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.mux(data, microseconds, isKeyframe);
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
- 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);
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
- 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);
33901
34246
  if (!annexB) continue;
34247
+ const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
33902
34248
  if (!seenKeyframe) {
33903
- if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
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
- socket.write(annexB);
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 wire-ready Annex-B.
33939
- * Audio frames are skipped raw TCP carries only video (Annex-B).
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
- convertFrame(frame) {
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
- const wireData = this.convertFrame(frame);
34044
- if (!wireData || wireData.length === 0) return;
34045
- 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;
34046
34476
  this.prebuffer.push({
34047
- data: Buffer.from(wireData),
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.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
+ }
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
- const udpErrors = [];
36624
- for (const m of methodsToTry) {
36625
- try {
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 udpApi.getInfo();
36651
- const capabilities = await udpApi.getDeviceCapabilities();
36652
- 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
+ ]);
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
- } catch (e) {
36692
- const msg = fmtErr(e);
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 runProbeVariants(
36752
- "getInfo",
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: "cmd80 class=0x6414",
36756
- op: () => api.getInfo(void 0, {
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: "cmd80 class=0x6614",
36763
- op: () => api.getInfo(void 0, {
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 udpApi.getInfo();
36891
- const capabilities = await udpApi.getDeviceCapabilities();
36892
- 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
+ ]);
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 udpErrors = [];
36937
- for (const m of methodsToTry) {
36938
- 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) => {
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
- ...inputs,
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 await detectOverUdpApi(udpApi, m);
36968
- } catch (e) {
36969
- const msg = e?.message || e?.toString?.() || String(e);
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,