@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.
@@ -1851,7 +1851,7 @@ var init_BaichuanVideoStream = __esm({
1851
1851
  const allowMsgNum0Fallback = this.acceptAnyStreamType && frame.header.msgNum === 0;
1852
1852
  if (!allowMsgNum0Fallback) {
1853
1853
  const frameCount = this._msgNumMismatchCount = (this._msgNumMismatchCount || 0) + 1;
1854
- if (frameCount <= 5) {
1854
+ if (frameCount <= 5 && this.client.getDebugConfig().general) {
1855
1855
  this.logger?.log(
1856
1856
  `[BaichuanVideoStream] Frame msgNum mismatch: received=${frame.header.msgNum}, expected=${this.activeMsgNum}, channel=${this.channel}, profile=${this.profile}, variant=${this.variant} (frame discarded)`
1857
1857
  );
@@ -1861,7 +1861,7 @@ var init_BaichuanVideoStream = __esm({
1861
1861
  }
1862
1862
  if (!this.acceptAnyStreamType && !this.expectedStreamTypes.has(frame.header.streamType)) {
1863
1863
  const frameCount = this._streamTypeMismatchCount = (this._streamTypeMismatchCount || 0) + 1;
1864
- if (frameCount <= 5) {
1864
+ if (frameCount <= 5 && this.client.getDebugConfig().general) {
1865
1865
  this.logger?.log(
1866
1866
  `[BaichuanVideoStream] Frame streamType mismatch: received=${frame.header.streamType}, expectedAny=[${[
1867
1867
  ...this.expectedStreamTypes
@@ -7815,19 +7815,34 @@ async function* createNativeStream(api, channel, profile, options) {
7815
7815
  }
7816
7816
  });
7817
7817
  streamStarted = true;
7818
- while (!closed) {
7818
+ const signal = options?.signal;
7819
+ while (!closed && !signal?.aborted) {
7819
7820
  if (frameQueue.length > 0) {
7820
7821
  const frame = frameQueue.shift();
7821
7822
  yield frame;
7822
7823
  } else {
7823
7824
  await new Promise((resolve) => {
7824
7825
  frameResolve = resolve;
7825
- setTimeout(() => {
7826
+ const timer = setTimeout(() => {
7826
7827
  if (frameResolve === resolve) {
7827
7828
  frameResolve = null;
7828
7829
  resolve();
7829
7830
  }
7830
7831
  }, 1e3);
7832
+ if (signal) {
7833
+ const onAbort = () => {
7834
+ clearTimeout(timer);
7835
+ if (frameResolve === resolve) frameResolve = null;
7836
+ resolve();
7837
+ };
7838
+ if (signal.aborted) {
7839
+ clearTimeout(timer);
7840
+ frameResolve = null;
7841
+ resolve();
7842
+ } else {
7843
+ signal.addEventListener("abort", onAbort, { once: true });
7844
+ }
7845
+ }
7831
7846
  });
7832
7847
  }
7833
7848
  }
@@ -8000,13 +8015,14 @@ var NativeStreamFanout = class {
8000
8015
  source = null;
8001
8016
  running = false;
8002
8017
  pumpPromise = null;
8018
+ abort = new AbortController();
8003
8019
  constructor(opts) {
8004
8020
  this.opts = opts;
8005
8021
  }
8006
8022
  start() {
8007
8023
  if (this.running) return;
8008
8024
  this.running = true;
8009
- this.source = this.opts.createSource();
8025
+ this.source = this.opts.createSource(this.abort.signal);
8010
8026
  this.pumpPromise = (async () => {
8011
8027
  try {
8012
8028
  for await (const frame of this.source) {
@@ -8052,6 +8068,7 @@ var NativeStreamFanout = class {
8052
8068
  this.source = null;
8053
8069
  for (const q of this.queues.values()) q.close();
8054
8070
  this.queues.clear();
8071
+ this.abort.abort();
8055
8072
  try {
8056
8073
  await src?.return(void 0);
8057
8074
  } catch {
@@ -8090,9 +8107,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8090
8107
  requireAuth;
8091
8108
  authNonces = /* @__PURE__ */ new Map();
8092
8109
  // Track nonces per client
8093
- AUTH_REALM = "BaichuanRtspServer";
8110
+ AUTH_REALM;
8094
8111
  NONCE_TIMEOUT_MS = 3e5;
8095
8112
  // 5 minutes
8113
+ lazyMetadata;
8096
8114
  // Client tracking
8097
8115
  connectedClients = /* @__PURE__ */ new Set();
8098
8116
  // Set of client IDs (IP:port)
@@ -8104,8 +8122,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8104
8122
  // Track all client resources for cleanup
8105
8123
  clientResources = /* @__PURE__ */ new Map();
8106
8124
  isRtspDebugEnabled() {
8107
- const dbg = this.api.client.getDebugConfig();
8108
- return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8125
+ try {
8126
+ if (this.api.isClosed) {
8127
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8128
+ }
8129
+ const dbg = this.api.client.getDebugConfig();
8130
+ return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8131
+ } catch {
8132
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
8133
+ }
8109
8134
  }
8110
8135
  rtspDebugLog(message) {
8111
8136
  if (!this.isRtspDebugEnabled()) return;
@@ -8127,10 +8152,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8127
8152
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
8128
8153
  nativeFanout = null;
8129
8154
  noClientAutoStopTimer;
8155
+ /** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
8156
+ noFrameDeadlineTimer;
8130
8157
  /** After last RTSP client; 0 = never auto-stop native stream. */
8131
8158
  nativeStreamIdleStopMs;
8132
8159
  /** Primed-but-no-PLAY timeout; 0 = disabled. */
8133
8160
  nativeStreamPrimeIdleStopMs;
8161
+ /**
8162
+ * Max time to wait for the first camera frame after stream start.
8163
+ * If no frames arrive within this window, the native stream is stopped
8164
+ * (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
8165
+ * firing and waking the camera when no real viewer is watching.
8166
+ * 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
8167
+ */
8168
+ nativeStreamNoFrameDeadlineMs;
8134
8169
  // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
8135
8170
  // When a new client connects while the stream is already running it does not need
8136
8171
  // to wait up to one full GOP interval for the next keyframe — we replay frames
@@ -8256,14 +8291,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8256
8291
  this.logger = options.logger ?? console;
8257
8292
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
8258
8293
  this.deviceId = options.deviceId;
8259
- this.externalListener = options.externalListener ?? false;
8294
+ this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
8260
8295
  this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
8261
8296
  this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
8262
- this.authCredentials = options.credentials ?? [];
8297
+ this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
8298
+ this.authCredentials = (options.credentials ?? []).map((c) => ({
8299
+ username: c.username,
8300
+ ...c.password !== void 0 ? { password: c.password } : {},
8301
+ ...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
8302
+ }));
8263
8303
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
8304
+ this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
8305
+ this.lazyMetadata = options.lazyMetadata ?? false;
8264
8306
  const transport = this.api.client.getTransport();
8265
8307
  this.flow = createRtspFlow(transport, "H264");
8266
8308
  }
8309
+ /** Number of currently connected RTSP clients. */
8310
+ get clientCount() {
8311
+ return this.connectedClients.size;
8312
+ }
8267
8313
  // --- Authentication helpers ---
8268
8314
  /**
8269
8315
  * Generate a new nonce for Digest authentication
@@ -8324,9 +8370,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8324
8370
  this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
8325
8371
  return false;
8326
8372
  }
8373
+ if (realm !== this.AUTH_REALM) {
8374
+ this.rtspDebugLog(
8375
+ `Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
8376
+ );
8377
+ return false;
8378
+ }
8327
8379
  for (const cred of this.authCredentials) {
8328
8380
  if (username !== cred.username) continue;
8329
- const ha1 = this.md5(`${cred.username}:${realm}:${cred.password}`);
8381
+ const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
8382
+ if (!ha1) continue;
8330
8383
  const ha2 = this.md5(`${method}:${authUri || uri}`);
8331
8384
  const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
8332
8385
  if (response === expectedResponse) {
@@ -8355,6 +8408,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8355
8408
  this.noClientAutoStopTimer = void 0;
8356
8409
  }
8357
8410
  }
8411
+ clearNoFrameDeadlineTimer() {
8412
+ if (this.noFrameDeadlineTimer) {
8413
+ clearTimeout(this.noFrameDeadlineTimer);
8414
+ this.noFrameDeadlineTimer = void 0;
8415
+ }
8416
+ }
8358
8417
  setFlowVideoType(videoType, reason) {
8359
8418
  if (this.flow.videoType === videoType) return;
8360
8419
  const transport = this.api.client.getTransport();
@@ -8369,25 +8428,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8369
8428
  if (this.active) {
8370
8429
  throw new Error("RTSP server is already active");
8371
8430
  }
8372
- try {
8373
- const metadata = await this.api.getStreamMetadata(this.channel);
8374
- const stream = metadata.streams.find((s) => s.profile === this.profile);
8375
- if (stream) {
8376
- this.streamMetadata = {
8377
- frameRate: stream.frameRate || 25,
8378
- width: stream.width,
8379
- height: stream.height
8380
- };
8381
- const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
8382
- const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
8383
- this.setFlowVideoType(metaVideoType, "metadata");
8384
- }
8385
- } catch (error) {
8386
- this.logger.warn(
8387
- `[BaichuanRtspServer] Could not get stream metadata: ${error}`
8431
+ if (this.lazyMetadata) {
8432
+ this.logger.info(
8433
+ `[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
8388
8434
  );
8389
- this.streamMetadata = { frameRate: 25 };
8390
- this.setFlowVideoType("H264", "metadata unavailable");
8435
+ } else {
8436
+ try {
8437
+ const metadata = await this.api.getStreamMetadata(this.channel);
8438
+ const stream = metadata.streams.find((s) => s.profile === this.profile);
8439
+ if (stream) {
8440
+ this.streamMetadata = {
8441
+ frameRate: stream.frameRate || 25,
8442
+ width: stream.width,
8443
+ height: stream.height
8444
+ };
8445
+ const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
8446
+ const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
8447
+ this.setFlowVideoType(metaVideoType, "metadata");
8448
+ }
8449
+ } catch (error) {
8450
+ this.logger.warn(
8451
+ `[BaichuanRtspServer] Could not get stream metadata: ${error}`
8452
+ );
8453
+ this.streamMetadata = { frameRate: 25 };
8454
+ this.setFlowVideoType("H264", "metadata unavailable");
8455
+ }
8391
8456
  }
8392
8457
  if (!this.externalListener) {
8393
8458
  this.clientConnectionServer = net.createServer((socket) => {
@@ -8428,6 +8493,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8428
8493
  }
8429
8494
  this.handleRtspConnection(socket, initialBuffer);
8430
8495
  }
8496
+ /**
8497
+ * Inject an already-accepted client socket from a multiplexer
8498
+ * (e.g. `LocalRtspMux`) that owns the listening port.
8499
+ *
8500
+ * The mux reads the first RTSP request line to determine the target path,
8501
+ * then hands the socket over. Any bytes already consumed during routing
8502
+ * are replayed back onto the socket via `unshift()` so the RTSP parser in
8503
+ * `handleRtspConnection` sees the complete original request.
8504
+ *
8505
+ * @param socket - Client TCP socket, already accepted by the mux.
8506
+ * @param preReadData - Bytes the mux has already pulled off the socket
8507
+ * while parsing the request line. Replayed via `socket.unshift()`
8508
+ * before any further reads.
8509
+ */
8510
+ injectSocket(socket, preReadData) {
8511
+ if (!this.active) {
8512
+ socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
8513
+ return;
8514
+ }
8515
+ if (preReadData && preReadData.length > 0) {
8516
+ socket.unshift(preReadData);
8517
+ }
8518
+ this.handleRtspConnection(socket);
8519
+ }
8431
8520
  /**
8432
8521
  * Handle RTSP connection from a client.
8433
8522
  */
@@ -8592,6 +8681,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8592
8681
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
8593
8682
  });
8594
8683
  } else if (method === "DESCRIBE") {
8684
+ if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
8685
+ void this.api.ensureConnected().catch(() => {
8686
+ });
8687
+ }
8595
8688
  if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
8596
8689
  try {
8597
8690
  if (!this.nativeStreamActive) {
@@ -8629,6 +8722,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8629
8722
  }
8630
8723
  }
8631
8724
  }
8725
+ if (!this.hasAudio && this.firstAudioPromise) {
8726
+ const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
8727
+ const audioPrimingStart = Date.now();
8728
+ try {
8729
+ await Promise.race([
8730
+ this.firstAudioPromise,
8731
+ new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
8732
+ ]);
8733
+ } catch {
8734
+ }
8735
+ const audioPrimingElapsed = Date.now() - audioPrimingStart;
8736
+ if (this.hasAudio) {
8737
+ this.logger.info(
8738
+ `[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
8739
+ );
8740
+ } else {
8741
+ this.logger.info(
8742
+ `[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
8743
+ );
8744
+ }
8745
+ }
8632
8746
  {
8633
8747
  const { fmtp, hasParamSets } = this.flow.getFmtp();
8634
8748
  const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
@@ -8637,12 +8751,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8637
8751
  );
8638
8752
  }
8639
8753
  const sdp = this.generateSdp();
8754
+ const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
8640
8755
  sendResponse(
8641
8756
  200,
8642
8757
  "OK",
8643
8758
  {
8644
8759
  "Content-Type": "application/sdp",
8645
- "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
8760
+ "Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
8646
8761
  },
8647
8762
  sdp
8648
8763
  );
@@ -8665,7 +8780,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8665
8780
  this.emit("client", clientId);
8666
8781
  this.clearNoClientAutoStopTimer();
8667
8782
  if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
8668
- await this.startNativeStream();
8783
+ void this.startNativeStream();
8669
8784
  }
8670
8785
  const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
8671
8786
  const transport = (transportMatch?.[1] ?? "").trim();
@@ -8799,12 +8914,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
8799
8914
  }
8800
8915
  }
8801
8916
  };
8917
+ const runProcessBuffer = () => {
8918
+ processBuffer().catch((err) => {
8919
+ this.logger.debug(
8920
+ `[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
8921
+ );
8922
+ try {
8923
+ socket.destroy();
8924
+ } catch {
8925
+ }
8926
+ });
8927
+ };
8802
8928
  socket.on("data", (data) => {
8803
8929
  buffer = Buffer.concat([buffer, data]);
8804
- void processBuffer();
8930
+ runProcessBuffer();
8805
8931
  });
8806
8932
  if (buffer.includes("\r\n\r\n")) {
8807
- void processBuffer();
8933
+ runProcessBuffer();
8808
8934
  }
8809
8935
  }
8810
8936
  /**
@@ -9672,6 +9798,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9672
9798
  if (this.nativeStreamActive) {
9673
9799
  return;
9674
9800
  }
9801
+ if (!this.api.isReady) {
9802
+ if (this.api.isClosed) {
9803
+ this.logger.warn?.(
9804
+ `[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
9805
+ );
9806
+ return;
9807
+ }
9808
+ try {
9809
+ this.logger.info?.(
9810
+ `[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
9811
+ );
9812
+ await this.api.ensureConnected();
9813
+ } catch (e) {
9814
+ this.logger.warn?.(
9815
+ `[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
9816
+ );
9817
+ return;
9818
+ }
9819
+ }
9675
9820
  this.nativeStreamActive = true;
9676
9821
  this.firstFrameReceived = false;
9677
9822
  this.firstAudioDetected = false;
@@ -9706,13 +9851,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9706
9851
  await this.flow.startKeepAlive(this.api);
9707
9852
  this.nativeFanout = new NativeStreamFanout({
9708
9853
  maxQueueItems: 200,
9709
- createSource: () => createNativeStream(this.api, this.channel, this.profile, {
9854
+ createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
9710
9855
  variant: this.variant,
9711
- ...dedicatedClient ? { client: dedicatedClient } : {}
9856
+ ...dedicatedClient ? { client: dedicatedClient } : {},
9857
+ signal
9712
9858
  }),
9713
9859
  onFrame: (frame) => {
9714
9860
  if (frame.audio) {
9715
- if (!this.hasAudio && this.api.client.getTransport() === "tcp" && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
9861
+ if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
9716
9862
  const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
9717
9863
  if (info) {
9718
9864
  this.hasAudio = true;
@@ -9761,6 +9907,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9761
9907
  onEnd: () => {
9762
9908
  if (!this.nativeStreamActive) return;
9763
9909
  this.nativeStreamActive = false;
9910
+ this.clearNoFrameDeadlineTimer();
9911
+ const hadFrames = this.firstFrameReceived;
9764
9912
  this.firstFrameReceived = false;
9765
9913
  this.firstFramePromise = null;
9766
9914
  this.firstFrameResolve = null;
@@ -9785,7 +9933,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9785
9933
  } catch {
9786
9934
  }
9787
9935
  }
9788
- if (this.connectedClients.size > 0) {
9936
+ if (this.connectedClients.size > 0 && hadFrames) {
9789
9937
  this.logger.info(
9790
9938
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
9791
9939
  );
@@ -9797,6 +9945,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9797
9945
  }
9798
9946
  });
9799
9947
  this.nativeFanout.start();
9948
+ this.clearNoFrameDeadlineTimer();
9949
+ if (this.nativeStreamNoFrameDeadlineMs > 0) {
9950
+ this.noFrameDeadlineTimer = setTimeout(() => {
9951
+ this.noFrameDeadlineTimer = void 0;
9952
+ if (!this.firstFrameReceived && this.nativeStreamActive) {
9953
+ this.logger.info(
9954
+ `[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
9955
+ );
9956
+ void this.stopNativeStream();
9957
+ }
9958
+ }, this.nativeStreamNoFrameDeadlineMs);
9959
+ this.noFrameDeadlineTimer?.unref?.();
9960
+ }
9800
9961
  this.clearNoClientAutoStopTimer();
9801
9962
  if (this.nativeStreamPrimeIdleStopMs > 0) {
9802
9963
  this.noClientAutoStopTimer = setTimeout(() => {
@@ -9813,6 +9974,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9813
9974
  markFirstFrameReceived() {
9814
9975
  if (!this.firstFrameReceived && this.firstFrameResolve) {
9815
9976
  this.firstFrameReceived = true;
9977
+ this.clearNoFrameDeadlineTimer();
9816
9978
  this.rtspDebugLog(
9817
9979
  `First frame received from camera for profile ${this.profile}`
9818
9980
  );
@@ -9839,6 +10001,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
9839
10001
  );
9840
10002
  this.flow.stopKeepAlive();
9841
10003
  this.clearNoClientAutoStopTimer();
10004
+ this.clearNoFrameDeadlineTimer();
9842
10005
  this.nativeStreamActive = false;
9843
10006
  this.firstFrameReceived = false;
9844
10007
  this.firstFramePromise = null;
@@ -10055,149 +10218,17 @@ init_BcMediaAnnexBDecoder();
10055
10218
  // src/baichuan/stream/MpegTsMuxer.ts
10056
10219
  var TS_PACKET_SIZE = 188;
10057
10220
  var TS_SYNC_BYTE = 71;
10058
- var PAT_PID = 0;
10059
- var PMT_PID = 4096;
10060
- var VIDEO_PID = 256;
10221
+ var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
10222
+ var PID_PAT = 0;
10223
+ var PID_PMT = 4096;
10224
+ var PID_VIDEO = 256;
10225
+ var PID_AUDIO = 257;
10061
10226
  var STREAM_TYPE_H264 = 27;
10062
10227
  var STREAM_TYPE_H265 = 36;
10063
- var patCc = 0;
10064
- var pmtCc = 0;
10065
- var videoCc = 0;
10066
- function createPat() {
10067
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
10068
- packet[0] = TS_SYNC_BYTE;
10069
- packet[1] = 64 | PAT_PID >> 8 & 31;
10070
- packet[2] = PAT_PID & 255;
10071
- packet[3] = 16 | patCc & 15;
10072
- patCc = patCc + 1 & 15;
10073
- packet[4] = 0;
10074
- let idx = 5;
10075
- packet[idx++] = 0;
10076
- packet[idx++] = 176;
10077
- packet[idx++] = 13;
10078
- packet[idx++] = 0;
10079
- packet[idx++] = 1;
10080
- packet[idx++] = 193;
10081
- packet[idx++] = 0;
10082
- packet[idx++] = 0;
10083
- packet[idx++] = 0;
10084
- packet[idx++] = 1;
10085
- packet[idx++] = 224 | PMT_PID >> 8 & 31;
10086
- packet[idx++] = PMT_PID & 255;
10087
- const crc = crc32Mpeg(packet.subarray(5, idx));
10088
- packet.writeUInt32BE(crc, idx);
10089
- return packet;
10090
- }
10091
- function createPmt(streamType) {
10092
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
10093
- packet[0] = TS_SYNC_BYTE;
10094
- packet[1] = 64 | PMT_PID >> 8 & 31;
10095
- packet[2] = PMT_PID & 255;
10096
- packet[3] = 16 | pmtCc & 15;
10097
- pmtCc = pmtCc + 1 & 15;
10098
- packet[4] = 0;
10099
- let idx = 5;
10100
- packet[idx++] = 2;
10101
- packet[idx++] = 176;
10102
- packet[idx++] = 18;
10103
- packet[idx++] = 0;
10104
- packet[idx++] = 1;
10105
- packet[idx++] = 193;
10106
- packet[idx++] = 0;
10107
- packet[idx++] = 0;
10108
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
10109
- packet[idx++] = VIDEO_PID & 255;
10110
- packet[idx++] = 240;
10111
- packet[idx++] = 0;
10112
- packet[idx++] = streamType;
10113
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
10114
- packet[idx++] = VIDEO_PID & 255;
10115
- packet[idx++] = 240;
10116
- packet[idx++] = 0;
10117
- const crc = crc32Mpeg(packet.subarray(5, idx));
10118
- packet.writeUInt32BE(crc, idx);
10119
- return packet;
10120
- }
10121
- function createVideoPes(data, pts, isKeyframe) {
10122
- const packets = [];
10123
- const pts90k = Math.floor(pts * 9e4 / 1e6);
10124
- const pesHeaderLen = 14;
10125
- const pesHeader = Buffer.alloc(pesHeaderLen);
10126
- let idx = 0;
10127
- pesHeader[idx++] = 0;
10128
- pesHeader[idx++] = 0;
10129
- pesHeader[idx++] = 1;
10130
- pesHeader[idx++] = 224;
10131
- pesHeader[idx++] = 0;
10132
- pesHeader[idx++] = 0;
10133
- pesHeader[idx++] = 128;
10134
- pesHeader[idx++] = 128;
10135
- pesHeader[idx++] = 5;
10136
- pesHeader[idx++] = 33 | pts90k >> 29 & 14;
10137
- pesHeader[idx++] = pts90k >> 22 & 255;
10138
- pesHeader[idx++] = 1 | pts90k >> 14 & 254;
10139
- pesHeader[idx++] = pts90k >> 7 & 255;
10140
- pesHeader[idx++] = 1 | pts90k << 1 & 254;
10141
- const pesData = Buffer.concat([pesHeader, data]);
10142
- let pesOffset = 0;
10143
- let isFirst = true;
10144
- while (pesOffset < pesData.length) {
10145
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
10146
- let pktIdx = 0;
10147
- packet[pktIdx++] = TS_SYNC_BYTE;
10148
- packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
10149
- packet[pktIdx++] = VIDEO_PID & 255;
10150
- const remaining = pesData.length - pesOffset;
10151
- const maxPayload = TS_PACKET_SIZE - 4;
10152
- if (remaining >= maxPayload) {
10153
- packet[pktIdx++] = 16 | videoCc & 15;
10154
- videoCc = videoCc + 1 & 15;
10155
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
10156
- pesOffset += maxPayload;
10157
- } else {
10158
- const adaptLen = maxPayload - remaining - 1;
10159
- if (adaptLen < 0) {
10160
- packet[pktIdx++] = 48 | videoCc & 15;
10161
- videoCc = videoCc + 1 & 15;
10162
- packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
10163
- if (isFirst && isKeyframe) {
10164
- packet[pktIdx++] = 64;
10165
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
10166
- packet[i] = 255;
10167
- }
10168
- } else {
10169
- packet[pktIdx++] = 0;
10170
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
10171
- packet[i] = 255;
10172
- }
10173
- }
10174
- pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
10175
- pesOffset += remaining;
10176
- } else {
10177
- packet[pktIdx++] = 48 | videoCc & 15;
10178
- videoCc = videoCc + 1 & 15;
10179
- if (adaptLen === 0) {
10180
- packet[pktIdx++] = 0;
10181
- } else {
10182
- packet[pktIdx++] = adaptLen;
10183
- if (isFirst && isKeyframe) {
10184
- packet[pktIdx++] = 64;
10185
- } else {
10186
- packet[pktIdx++] = 0;
10187
- }
10188
- for (let i = 0; i < adaptLen - 1; i++) {
10189
- packet[pktIdx++] = 255;
10190
- }
10191
- }
10192
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
10193
- pesOffset += remaining;
10194
- }
10195
- }
10196
- packets.push(packet);
10197
- isFirst = false;
10198
- }
10199
- return packets;
10200
- }
10228
+ var STREAM_TYPE_AAC = 15;
10229
+ var PES_STREAM_ID_VIDEO = 224;
10230
+ var PES_STREAM_ID_AUDIO = 192;
10231
+ var PAT_PMT_INTERVAL = 40;
10201
10232
  function crc32Mpeg(data) {
10202
10233
  let crc = 4294967295;
10203
10234
  for (let i = 0; i < data.length; i++) {
@@ -10212,45 +10243,218 @@ function crc32Mpeg(data) {
10212
10243
  }
10213
10244
  return crc >>> 0;
10214
10245
  }
10246
+ function usToPts(us) {
10247
+ return Math.floor(us * 90 / 1e3) & 8589934591;
10248
+ }
10249
+ function encodePts(buf, offset, pts, prefix) {
10250
+ buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
10251
+ buf[offset + 1] = pts >>> 22 & 255;
10252
+ buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
10253
+ buf[offset + 3] = pts >>> 7 & 255;
10254
+ buf[offset + 4] = (pts & 127) << 1 | 1;
10255
+ }
10256
+ function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
10257
+ buf[0] = TS_SYNC_BYTE;
10258
+ buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
10259
+ buf[2] = pid & 255;
10260
+ buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
10261
+ }
10262
+ function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
10263
+ const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
10264
+ const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
10265
+ let pesOffset = 0;
10266
+ let outOffset = 0;
10267
+ let isFirst = true;
10268
+ while (pesOffset < pesData.length) {
10269
+ const remaining = pesData.length - pesOffset;
10270
+ const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
10271
+ outOffset += TS_PACKET_SIZE;
10272
+ if (remaining >= TS_PAYLOAD_SIZE) {
10273
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
10274
+ ccRef.cc = ccRef.cc + 1 & 15;
10275
+ pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
10276
+ pesOffset += TS_PAYLOAD_SIZE;
10277
+ } else {
10278
+ const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
10279
+ if (paddingNeeded === 1) {
10280
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
10281
+ ccRef.cc = ccRef.cc + 1 & 15;
10282
+ packet[4] = 0;
10283
+ pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
10284
+ } else {
10285
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
10286
+ ccRef.cc = ccRef.cc + 1 & 15;
10287
+ const adaptLen = paddingNeeded - 1;
10288
+ packet[4] = adaptLen;
10289
+ packet[5] = isFirst && isKeyframe ? 64 : 0;
10290
+ packet.fill(255, 6, 4 + paddingNeeded);
10291
+ pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
10292
+ }
10293
+ pesOffset += remaining;
10294
+ }
10295
+ isFirst = false;
10296
+ }
10297
+ return out;
10298
+ }
10299
+ function buildPat(cc) {
10300
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
10301
+ pkt[0] = TS_SYNC_BYTE;
10302
+ pkt[1] = 64 | PID_PAT >> 8 & 31;
10303
+ pkt[2] = PID_PAT & 255;
10304
+ pkt[3] = 16 | cc & 15;
10305
+ pkt[4] = 0;
10306
+ const sectionStart = 5;
10307
+ let i = sectionStart;
10308
+ pkt[i++] = 0;
10309
+ pkt[i++] = 176;
10310
+ pkt[i++] = 13;
10311
+ pkt[i++] = 0;
10312
+ pkt[i++] = 1;
10313
+ pkt[i++] = 193;
10314
+ pkt[i++] = 0;
10315
+ pkt[i++] = 0;
10316
+ pkt[i++] = 0;
10317
+ pkt[i++] = 1;
10318
+ pkt[i++] = 224 | PID_PMT >> 8 & 31;
10319
+ pkt[i++] = PID_PMT & 255;
10320
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
10321
+ pkt.writeUInt32BE(crc, i);
10322
+ return pkt;
10323
+ }
10324
+ function buildPmt(videoStreamType, includeAudio, cc) {
10325
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
10326
+ pkt[0] = TS_SYNC_BYTE;
10327
+ pkt[1] = 64 | PID_PMT >> 8 & 31;
10328
+ pkt[2] = PID_PMT & 255;
10329
+ pkt[3] = 16 | cc & 15;
10330
+ pkt[4] = 0;
10331
+ const sectionStart = 5;
10332
+ let i = sectionStart;
10333
+ pkt[i++] = 2;
10334
+ pkt[i++] = 176;
10335
+ const sectionLenPos = i;
10336
+ i += 1;
10337
+ pkt[i++] = 0;
10338
+ pkt[i++] = 1;
10339
+ pkt[i++] = 193;
10340
+ pkt[i++] = 0;
10341
+ pkt[i++] = 0;
10342
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
10343
+ pkt[i++] = PID_VIDEO & 255;
10344
+ pkt[i++] = 240;
10345
+ pkt[i++] = 0;
10346
+ pkt[i++] = videoStreamType;
10347
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
10348
+ pkt[i++] = PID_VIDEO & 255;
10349
+ pkt[i++] = 240;
10350
+ pkt[i++] = 0;
10351
+ if (includeAudio) {
10352
+ pkt[i++] = STREAM_TYPE_AAC;
10353
+ pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
10354
+ pkt[i++] = PID_AUDIO & 255;
10355
+ pkt[i++] = 240;
10356
+ pkt[i++] = 0;
10357
+ }
10358
+ const sectionLen = i - sectionStart - 3 + 4;
10359
+ pkt[sectionLenPos] = sectionLen;
10360
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
10361
+ pkt.writeUInt32BE(crc, i);
10362
+ return pkt;
10363
+ }
10364
+ function buildVideoPes(annexBData, ptsUs, isKeyframe) {
10365
+ const pts = usToPts(ptsUs);
10366
+ const pesHeader = Buffer.allocUnsafe(14);
10367
+ pesHeader[0] = 0;
10368
+ pesHeader[1] = 0;
10369
+ pesHeader[2] = 1;
10370
+ pesHeader[3] = PES_STREAM_ID_VIDEO;
10371
+ pesHeader[4] = 0;
10372
+ pesHeader[5] = 0;
10373
+ pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
10374
+ pesHeader[7] = 128;
10375
+ pesHeader[8] = 5;
10376
+ encodePts(pesHeader, 9, pts, 2);
10377
+ return Buffer.concat([pesHeader, annexBData]);
10378
+ }
10379
+ function buildAudioPes(adtsData, ptsUs) {
10380
+ const pts = usToPts(ptsUs);
10381
+ const pesPayloadLen = 8 + adtsData.length;
10382
+ const pesHeader = Buffer.allocUnsafe(14);
10383
+ pesHeader[0] = 0;
10384
+ pesHeader[1] = 0;
10385
+ pesHeader[2] = 1;
10386
+ pesHeader[3] = PES_STREAM_ID_AUDIO;
10387
+ pesHeader[4] = pesPayloadLen >> 8 & 255;
10388
+ pesHeader[5] = pesPayloadLen & 255;
10389
+ pesHeader[6] = 128;
10390
+ pesHeader[7] = 128;
10391
+ pesHeader[8] = 5;
10392
+ encodePts(pesHeader, 9, pts, 2);
10393
+ return Buffer.concat([pesHeader, adtsData]);
10394
+ }
10215
10395
  var MpegTsMuxer = class {
10216
- streamType;
10217
- patSent = false;
10218
- pmtSent = false;
10219
- patPmtInterval = 0;
10220
- patPmtIntervalMax = 40;
10221
- // Send PAT/PMT every ~40 frames
10396
+ videoStreamType;
10397
+ includeAudio;
10398
+ // Per-instance continuity counters (4-bit, wrap at 16)
10399
+ patCc = 0;
10400
+ pmtCc = 0;
10401
+ videoCc = 0;
10402
+ audioCc = 0;
10403
+ framesSinceTableSend = 0;
10404
+ tablesSent = false;
10222
10405
  constructor(options) {
10223
- this.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
10406
+ this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
10407
+ this.includeAudio = options.includeAudio ?? true;
10224
10408
  }
10225
10409
  /**
10226
- * Reset continuity counters (call when starting a new stream).
10410
+ * Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
10411
+ * PAT and PMT are emitted before keyframes and periodically.
10412
+ *
10413
+ * @param annexBData - Annex-B video data (with start codes)
10414
+ * @param ptsUs - Presentation timestamp in microseconds
10415
+ * @param isKeyframe - Whether this is an IDR / IRAP frame
10227
10416
  */
10228
- static resetCounters() {
10229
- patCc = 0;
10230
- pmtCc = 0;
10231
- videoCc = 0;
10232
- }
10233
- /**
10234
- * Mux a video frame into MPEG-TS packets.
10417
+ muxVideo(annexBData, ptsUs, isKeyframe) {
10418
+ const chunks = [];
10419
+ const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
10420
+ if (needTables) {
10421
+ chunks.push(buildPat(this.patCc));
10422
+ this.patCc = this.patCc + 1 & 15;
10423
+ chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
10424
+ this.pmtCc = this.pmtCc + 1 & 15;
10425
+ this.tablesSent = true;
10426
+ this.framesSinceTableSend = 0;
10427
+ }
10428
+ this.framesSinceTableSend++;
10429
+ const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
10430
+ const ccRef = { cc: this.videoCc };
10431
+ chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
10432
+ this.videoCc = ccRef.cc;
10433
+ return Buffer.concat(chunks);
10434
+ }
10435
+ /**
10436
+ * Mux an audio frame (ADTS AAC) into MPEG-TS packets.
10437
+ * Returns an empty Buffer when includeAudio is false.
10235
10438
  *
10236
- * @param data - Annex-B video data (with start codes)
10237
- * @param microseconds - Frame timestamp in microseconds
10238
- * @param isKeyframe - Whether this is a keyframe
10239
- * @returns Buffer containing all TS packets for this frame
10240
- */
10241
- mux(data, microseconds, isKeyframe) {
10242
- const packets = [];
10243
- if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
10244
- packets.push(createPat());
10245
- packets.push(createPmt(this.streamType));
10246
- this.patSent = true;
10247
- this.pmtSent = true;
10248
- this.patPmtInterval = 0;
10249
- }
10250
- this.patPmtInterval++;
10251
- const videoPackets = createVideoPes(data, microseconds, isKeyframe);
10252
- packets.push(...videoPackets);
10253
- return Buffer.concat(packets);
10439
+ * @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
10440
+ * @param ptsUs - Presentation timestamp in microseconds
10441
+ */
10442
+ muxAudio(adtsData, ptsUs) {
10443
+ if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
10444
+ const pes = buildAudioPes(adtsData, ptsUs);
10445
+ const ccRef = { cc: this.audioCc };
10446
+ const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
10447
+ this.audioCc = ccRef.cc;
10448
+ return result;
10449
+ }
10450
+ /** Reset all continuity counters and table state (e.g. after stream restart). */
10451
+ reset() {
10452
+ this.patCc = 0;
10453
+ this.pmtCc = 0;
10454
+ this.videoCc = 0;
10455
+ this.audioCc = 0;
10456
+ this.framesSinceTableSend = 0;
10457
+ this.tablesSent = false;
10254
10458
  }
10255
10459
  };
10256
10460
 
@@ -15526,6 +15730,53 @@ var getAiStateViaGetAiAlarm = async (params) => {
15526
15730
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "getAiState failed"));
15527
15731
  };
15528
15732
 
15733
+ // src/reolink/baichuan/utils/sleepInference.ts
15734
+ function decideSleepInferenceTransition(input) {
15735
+ const { inferred, committed, pending, hysteresisPolls } = input;
15736
+ if (committed === void 0) {
15737
+ return {
15738
+ emit: inferred === "sleeping" ? "sleeping" : null,
15739
+ nextCommitted: inferred,
15740
+ nextPending: void 0
15741
+ };
15742
+ }
15743
+ if (inferred === committed) {
15744
+ return {
15745
+ emit: null,
15746
+ nextCommitted: committed,
15747
+ nextPending: void 0
15748
+ };
15749
+ }
15750
+ const effectivePolls = Math.max(1, hysteresisPolls);
15751
+ if (!pending || pending.state !== inferred) {
15752
+ if (effectivePolls <= 1) {
15753
+ return {
15754
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
15755
+ nextCommitted: inferred,
15756
+ nextPending: void 0
15757
+ };
15758
+ }
15759
+ return {
15760
+ emit: null,
15761
+ nextCommitted: committed,
15762
+ nextPending: { state: inferred, count: 1 }
15763
+ };
15764
+ }
15765
+ const nextCount = pending.count + 1;
15766
+ if (nextCount < effectivePolls) {
15767
+ return {
15768
+ emit: null,
15769
+ nextCommitted: committed,
15770
+ nextPending: { state: inferred, count: nextCount }
15771
+ };
15772
+ }
15773
+ return {
15774
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
15775
+ nextCommitted: inferred,
15776
+ nextPending: void 0
15777
+ };
15778
+ }
15779
+
15529
15780
  // src/reolink/baichuan/utils/channelInfoPush.ts
15530
15781
  init_xml();
15531
15782
  var parseOptionalInt = (value) => {
@@ -17802,7 +18053,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17802
18053
  statePollingInterval;
17803
18054
  udpSleepInferenceInterval;
17804
18055
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
18056
+ /**
18057
+ * Per-channel pending sleep-state candidate for hysteresis.
18058
+ * When the inference flips to a new state we require N consecutive polls
18059
+ * of that same state before committing it — this filters out transient
18060
+ * flapping caused by non-waking traffic drifting in/out of the 10 s
18061
+ * getSleepStatus() observation window during stream teardown.
18062
+ */
18063
+ udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
17805
18064
  udpSleepInferenceIntervalMs = 2e3;
18065
+ /** Consecutive inference polls required to commit a new sleeping/awake state. */
18066
+ udpSleepInferenceHysteresisPolls = 2;
17806
18067
  lastMotionState;
17807
18068
  lastAiState;
17808
18069
  aiStatePollingDisabled = false;
@@ -18269,6 +18530,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18269
18530
  */
18270
18531
  attachD2cDiscListener(client) {
18271
18532
  client.on("d2c_disc", () => this.notifyD2cDisc());
18533
+ client.on("error", () => {
18534
+ });
18272
18535
  }
18273
18536
  /**
18274
18537
  * Acquire a socket from the pool by tag.
@@ -18387,6 +18650,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18387
18650
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
18388
18651
  const newClient = new BaichuanClient(clientOpts);
18389
18652
  this.attachD2cDiscListener(newClient);
18653
+ newClient.on("error", (err) => {
18654
+ log?.debug?.(
18655
+ `[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
18656
+ );
18657
+ });
18390
18658
  await newClient.login();
18391
18659
  const existingCooldown = this.socketPoolCooldowns.get(this.host);
18392
18660
  if (existingCooldown) {
@@ -18902,6 +19170,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18902
19170
  * Only counts sessions from our own IP address.
18903
19171
  */
18904
19172
  async maybeRebootOnTooManySessions() {
19173
+ if (!this.client.isSocketConnected?.()) return;
18905
19174
  const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
18906
19175
  if (this.sessionGuardRebootInFlight) return;
18907
19176
  const cooldownMs = 10 * 6e4;
@@ -19343,6 +19612,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19343
19612
  }
19344
19613
  async renewSimpleEventSubscription() {
19345
19614
  if (this.simpleEventListeners.size === 0) return;
19615
+ if (!this.client.isSocketConnected?.()) return;
19346
19616
  if (this.simpleEventResubscribeInFlight)
19347
19617
  return await this.simpleEventResubscribeInFlight;
19348
19618
  this.simpleEventResubscribeInFlight = (async () => {
@@ -23110,23 +23380,32 @@ ${stderr}`)
23110
23380
  return;
23111
23381
  }
23112
23382
  const channel = this.client.getConfiguredChannel?.() ?? 0;
23383
+ if (!this.client.isSocketConnected?.()) {
23384
+ this.udpPendingSleepStateByChannel.delete(channel);
23385
+ return;
23386
+ }
23113
23387
  const status = this.getSleepStatus({ channel });
23114
23388
  if (status.state === "unknown") return;
23115
- const prev = this.udpLastInferredSleepStateByChannel.get(channel);
23116
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
23117
- if (prev === void 0) {
23118
- if (status.state === "sleeping") {
23119
- this.dispatchSimpleEvent({
23120
- type: "sleeping",
23121
- channel,
23122
- timestamp: Date.now()
23123
- });
23124
- }
23125
- return;
23389
+ const committed = this.udpLastInferredSleepStateByChannel.get(channel);
23390
+ const pending = this.udpPendingSleepStateByChannel.get(channel);
23391
+ const decision = decideSleepInferenceTransition({
23392
+ inferred: status.state,
23393
+ committed,
23394
+ pending,
23395
+ hysteresisPolls: this.udpSleepInferenceHysteresisPolls
23396
+ });
23397
+ this.udpLastInferredSleepStateByChannel.set(
23398
+ channel,
23399
+ decision.nextCommitted
23400
+ );
23401
+ if (decision.nextPending === void 0) {
23402
+ this.udpPendingSleepStateByChannel.delete(channel);
23403
+ } else {
23404
+ this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
23126
23405
  }
23127
- if (prev !== status.state) {
23406
+ if (decision.emit) {
23128
23407
  this.dispatchSimpleEvent({
23129
- type: status.state === "sleeping" ? "sleeping" : "awake",
23408
+ type: decision.emit,
23130
23409
  channel,
23131
23410
  timestamp: Date.now()
23132
23411
  });
@@ -23149,6 +23428,7 @@ ${stderr}`)
23149
23428
  this.udpSleepInferenceInterval = void 0;
23150
23429
  }
23151
23430
  this.udpLastInferredSleepStateByChannel.clear();
23431
+ this.udpPendingSleepStateByChannel.clear();
23152
23432
  }
23153
23433
  /**
23154
23434
  * GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
@@ -26747,8 +27027,8 @@ ${scheduleItems}
26747
27027
  );
26748
27028
  let args;
26749
27029
  if (useMpegTsMuxer) {
26750
- MpegTsMuxer.resetCounters();
26751
- tsMuxer = new MpegTsMuxer({ videoType });
27030
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
27031
+ tsMuxer.reset();
26752
27032
  args = [
26753
27033
  "-hide_banner",
26754
27034
  "-loglevel",
@@ -26912,7 +27192,7 @@ ${scheduleItems}
26912
27192
  startFfmpeg(videoType);
26913
27193
  frameCount++;
26914
27194
  if (useMpegTsMuxer && tsMuxer) {
26915
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
27195
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
26916
27196
  input.write(tsData);
26917
27197
  } else {
26918
27198
  if (videoType === "H264") input.write(H264_AUD);
@@ -27201,8 +27481,8 @@ ${scheduleItems}
27201
27481
  logger?.log?.(
27202
27482
  `[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
27203
27483
  );
27204
- MpegTsMuxer.resetCounters();
27205
- tsMuxer = new MpegTsMuxer({ videoType });
27484
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
27485
+ tsMuxer.reset();
27206
27486
  const args = [
27207
27487
  "-hide_banner",
27208
27488
  "-loglevel",
@@ -27367,7 +27647,7 @@ ${scheduleItems}
27367
27647
  startFfmpeg(videoType);
27368
27648
  frameCount++;
27369
27649
  if (tsMuxer) {
27370
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
27650
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
27371
27651
  input.write(tsData);
27372
27652
  }
27373
27653
  if (frameCount === 1) {
@@ -28202,9 +28482,10 @@ async function autoDetectDeviceType(inputs) {
28202
28482
  const msg = fmtErr(e);
28203
28483
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
28204
28484
  };
28205
- const withRetries = async (label, max, op, shouldRetry) => {
28485
+ const withRetries = async (label, max, op, shouldRetry, isAborted) => {
28206
28486
  let lastErr;
28207
28487
  for (let attempt = 1; attempt <= max; attempt++) {
28488
+ if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
28208
28489
  try {
28209
28490
  if (attempt > 1) {
28210
28491
  logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
@@ -28213,7 +28494,7 @@ async function autoDetectDeviceType(inputs) {
28213
28494
  } catch (e) {
28214
28495
  lastErr = e;
28215
28496
  const msg = fmtErr(e);
28216
- const retryable = attempt < max && shouldRetry(e);
28497
+ const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
28217
28498
  logger?.log?.(
28218
28499
  `[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
28219
28500
  );
@@ -28223,6 +28504,31 @@ async function autoDetectDeviceType(inputs) {
28223
28504
  }
28224
28505
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
28225
28506
  };
28507
+ const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
28508
+ let raceWon = false;
28509
+ const methodErrors = /* @__PURE__ */ new Map();
28510
+ try {
28511
+ return await Promise.any(
28512
+ methods.map(async (m) => {
28513
+ try {
28514
+ const result = await loginAndDetect(m, () => raceWon);
28515
+ raceWon = true;
28516
+ return result;
28517
+ } catch (e) {
28518
+ if (!raceWon) methodErrors.set(m, fmtErr(e));
28519
+ logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
28520
+ throw e;
28521
+ }
28522
+ })
28523
+ );
28524
+ } catch (e) {
28525
+ if (e instanceof AggregateError) {
28526
+ const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
28527
+ throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
28528
+ }
28529
+ throw e;
28530
+ }
28531
+ };
28226
28532
  const effectiveUid = normalizeUid(uid);
28227
28533
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
28228
28534
  const isReachable = await pingHost(host);
@@ -28252,9 +28558,9 @@ async function autoDetectDeviceType(inputs) {
28252
28558
  normalizedUid = normalizedDiscovered;
28253
28559
  }
28254
28560
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
28255
- const udpErrors = [];
28256
- for (const m of methodsToTry) {
28257
- try {
28561
+ return await runUdpMethodsParallel(
28562
+ methodsToTry,
28563
+ async (m, isAborted) => {
28258
28564
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
28259
28565
  const udpApi = await withRetries(
28260
28566
  `UDP(${m})`,
@@ -28277,11 +28583,14 @@ async function autoDetectDeviceType(inputs) {
28277
28583
  throw e;
28278
28584
  }
28279
28585
  },
28280
- shouldRetryUdp
28586
+ shouldRetryUdp,
28587
+ isAborted
28281
28588
  );
28282
- const deviceInfo = await udpApi.getInfo();
28283
- const capabilities = await udpApi.getDeviceCapabilities();
28284
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
28589
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
28590
+ udpApi.getInfo(),
28591
+ udpApi.getDeviceCapabilities(),
28592
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
28593
+ ]);
28285
28594
  const channelNum = capabilities?.support?.channelNum ?? 1;
28286
28595
  const model = deviceInfo.type?.trim();
28287
28596
  const normalizedModel = model ? model.trim() : void 0;
@@ -28320,14 +28629,8 @@ async function autoDetectDeviceType(inputs) {
28320
28629
  channelNum: 1,
28321
28630
  api: udpApi
28322
28631
  };
28323
- } catch (e) {
28324
- const msg = fmtErr(e);
28325
- udpErrors.push(`${m}: ${msg}`);
28326
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
28327
- }
28328
- }
28329
- throw new Error(
28330
- `Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
28632
+ },
28633
+ "Forced UDP autodetect failed for all methods."
28331
28634
  );
28332
28635
  }
28333
28636
  let tcpApi;
@@ -28380,54 +28683,57 @@ async function autoDetectDeviceType(inputs) {
28380
28683
  }
28381
28684
  return void 0;
28382
28685
  };
28383
- const infoProbe = await runProbeVariants(
28384
- "getInfo",
28385
- [
28686
+ const [infoProbe, supportProbe] = await Promise.all([
28687
+ runProbeVariants(
28688
+ "getInfo",
28689
+ [
28690
+ {
28691
+ variant: "cmd80 class=0x6414",
28692
+ op: () => api.getInfo(void 0, {
28693
+ timeoutMs: 2500,
28694
+ messageClass: BC_CLASS_MODERN_24
28695
+ })
28696
+ },
28697
+ {
28698
+ variant: "cmd80 class=0x6614",
28699
+ op: () => api.getInfo(void 0, {
28700
+ timeoutMs: 3e3,
28701
+ messageClass: BC_CLASS_MODERN_20
28702
+ })
28703
+ },
28704
+ {
28705
+ variant: "cmd318(ch0) class=0x6414",
28706
+ op: () => api.getInfo(0, {
28707
+ timeoutMs: 3e3,
28708
+ messageClass: BC_CLASS_MODERN_24
28709
+ })
28710
+ },
28711
+ {
28712
+ variant: "cmd318(ch0) class=0x6614",
28713
+ op: () => api.getInfo(0, {
28714
+ timeoutMs: 3500,
28715
+ messageClass: BC_CLASS_MODERN_20
28716
+ })
28717
+ }
28718
+ ]
28719
+ ),
28720
+ // Support probes (cmd 199). Some firmwares may not support it or are slow.
28721
+ runProbeVariants("getSupportInfo", [
28386
28722
  {
28387
- variant: "cmd80 class=0x6414",
28388
- op: () => api.getInfo(void 0, {
28723
+ variant: "cmd199 class=0x6414",
28724
+ op: () => api.getSupportInfo({
28389
28725
  timeoutMs: 2500,
28390
28726
  messageClass: BC_CLASS_MODERN_24
28391
28727
  })
28392
28728
  },
28393
28729
  {
28394
- variant: "cmd80 class=0x6614",
28395
- op: () => api.getInfo(void 0, {
28396
- timeoutMs: 3e3,
28397
- messageClass: BC_CLASS_MODERN_20
28398
- })
28399
- },
28400
- {
28401
- variant: "cmd318(ch0) class=0x6414",
28402
- op: () => api.getInfo(0, {
28403
- timeoutMs: 3e3,
28404
- messageClass: BC_CLASS_MODERN_24
28405
- })
28406
- },
28407
- {
28408
- variant: "cmd318(ch0) class=0x6614",
28409
- op: () => api.getInfo(0, {
28730
+ variant: "cmd199 class=0x6614",
28731
+ op: () => api.getSupportInfo({
28410
28732
  timeoutMs: 3500,
28411
28733
  messageClass: BC_CLASS_MODERN_20
28412
28734
  })
28413
28735
  }
28414
- ]
28415
- );
28416
- const supportProbe = await runProbeVariants("getSupportInfo", [
28417
- {
28418
- variant: "cmd199 class=0x6414",
28419
- op: () => api.getSupportInfo({
28420
- timeoutMs: 2500,
28421
- messageClass: BC_CLASS_MODERN_24
28422
- })
28423
- },
28424
- {
28425
- variant: "cmd199 class=0x6614",
28426
- op: () => api.getSupportInfo({
28427
- timeoutMs: 3500,
28428
- messageClass: BC_CLASS_MODERN_20
28429
- })
28430
- }
28736
+ ])
28431
28737
  ]);
28432
28738
  const deviceInfo = infoProbe?.value;
28433
28739
  const support = supportProbe?.value;
@@ -28519,9 +28825,11 @@ async function autoDetectDeviceType(inputs) {
28519
28825
  }
28520
28826
  try {
28521
28827
  const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
28522
- const deviceInfo = await udpApi.getInfo();
28523
- const capabilities = await udpApi.getDeviceCapabilities();
28524
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
28828
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
28829
+ udpApi.getInfo(),
28830
+ udpApi.getDeviceCapabilities(),
28831
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
28832
+ ]);
28525
28833
  const channelNum = capabilities?.support?.channelNum ?? 1;
28526
28834
  const model = deviceInfo.type?.trim();
28527
28835
  const normalizedModel = model ? model.trim() : void 0;
@@ -28565,21 +28873,17 @@ async function autoDetectDeviceType(inputs) {
28565
28873
  };
28566
28874
  };
28567
28875
  const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
28568
- const udpErrors = [];
28569
- for (const m of methodsToTry) {
28570
- try {
28876
+ const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
28877
+ return await runUdpMethodsParallel(
28878
+ viableMethods,
28879
+ async (m, isAborted) => {
28571
28880
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
28572
28881
  const udpApi = await withRetries(
28573
28882
  `UDP(${m})`,
28574
28883
  maxRetries,
28575
28884
  async (attempt) => {
28576
- const apiInputs = {
28577
- ...inputs,
28578
- udpDiscoveryMethod: m
28579
- };
28580
- if (normalizedUid) {
28581
- apiInputs.uid = normalizedUid;
28582
- }
28885
+ const apiInputs = { ...inputs, udpDiscoveryMethod: m };
28886
+ if (normalizedUid) apiInputs.uid = normalizedUid;
28583
28887
  const api = createBaichuanApi(apiInputs, "udp");
28584
28888
  try {
28585
28889
  await api.login();
@@ -28594,20 +28898,12 @@ async function autoDetectDeviceType(inputs) {
28594
28898
  throw e;
28595
28899
  }
28596
28900
  },
28597
- shouldRetryUdp
28901
+ shouldRetryUdp,
28902
+ isAborted
28598
28903
  );
28599
- return await detectOverUdpApi(udpApi, m);
28600
- } catch (e) {
28601
- const msg = e?.message || e?.toString?.() || String(e);
28602
- udpErrors.push(`${m}: ${msg}`);
28603
- try {
28604
- } catch {
28605
- }
28606
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
28607
- }
28608
- }
28609
- throw new Error(
28610
- `UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
28904
+ return detectOverUdpApi(udpApi, m);
28905
+ },
28906
+ "UDP discovery failed for all methods."
28611
28907
  );
28612
28908
  } catch (udpError) {
28613
28909
  logger?.log?.(