@apocaliss92/nodelink-js 0.4.5 → 0.4.6

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.
@@ -3,10 +3,10 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-WDFKIHM5.js";
6
+ } from "../chunk-F2Y5U3YP.js";
7
7
  import {
8
8
  __require
9
- } from "../chunk-DEOMUWBN.js";
9
+ } from "../chunk-TR3V5FTO.js";
10
10
 
11
11
  // src/cli/rtsp-server.ts
12
12
  function parseArgs() {
package/dist/index.cjs CHANGED
@@ -2178,6 +2178,19 @@ var init_BaichuanVideoStream = __esm({
2178
2178
  // Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
2179
2179
  // In CFB mode, continuation frames must use the cipher state from previous frames.
2180
2180
  aesStreamDecryptor = null;
2181
+ /**
2182
+ * Pending startup error stashed when emitSafeError is called before any
2183
+ * "error" listener is registered (e.g. camera returns 400 during start()).
2184
+ * The rfc4571-server's waitForKeyframe can consume this immediately instead
2185
+ * of waiting for the full keyframe timeout.
2186
+ */
2187
+ _pendingStartupError;
2188
+ /** Consume and clear any pending startup error. */
2189
+ consumePendingStartupError() {
2190
+ const err = this._pendingStartupError;
2191
+ this._pendingStartupError = void 0;
2192
+ return err;
2193
+ }
2181
2194
  emitSafeError(err) {
2182
2195
  if (!this.active) {
2183
2196
  this.logger?.warn?.(
@@ -2189,6 +2202,7 @@ var init_BaichuanVideoStream = __esm({
2189
2202
  this.logger?.warn?.(
2190
2203
  `[BaichuanVideoStream] Unhandled stream error: ${err.message}`
2191
2204
  );
2205
+ this._pendingStartupError = err;
2192
2206
  return;
2193
2207
  }
2194
2208
  this.emit("error", err);
@@ -8527,6 +8541,9 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
8527
8541
  resendTimer;
8528
8542
  hbTimer;
8529
8543
  discoveryTid;
8544
+ // Track discovery-phase timers so close() can cancel them even if
8545
+ // discovery is still in progress (prevents ERR_SOCKET_DGRAM_NOT_RUNNING).
8546
+ discoveryTimers = [];
8530
8547
  acceptSent = false;
8531
8548
  lastAcceptAtMs;
8532
8549
  ackScheduled = false;
@@ -8575,9 +8592,31 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
8575
8592
  });
8576
8593
  sock.on("error", (e) => this.emit("error", e));
8577
8594
  sock.on("close", () => this.emit("close"));
8578
- await new Promise(
8579
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
8580
- );
8595
+ const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
8596
+ for (let i = portRange.length - 1; i > 0; i--) {
8597
+ const j = Math.floor(Math.random() * (i + 1));
8598
+ [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
8599
+ }
8600
+ let bound = false;
8601
+ for (const port of portRange) {
8602
+ try {
8603
+ await new Promise((resolve, reject) => {
8604
+ sock.once("error", reject);
8605
+ sock.bind(port, "0.0.0.0", () => {
8606
+ sock.removeListener("error", reject);
8607
+ resolve();
8608
+ });
8609
+ });
8610
+ bound = true;
8611
+ break;
8612
+ } catch {
8613
+ }
8614
+ }
8615
+ if (!bound) {
8616
+ await new Promise(
8617
+ (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
8618
+ );
8619
+ }
8581
8620
  if (this.opts.mode === "direct") {
8582
8621
  this.remote = { host: this.opts.host, port: this.opts.port };
8583
8622
  this.clientId = this.opts.clientId;
@@ -8997,7 +9036,24 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
8997
9036
  BCUDP_DISCOVERY_PORT_LOCAL_ANY,
8998
9037
  BCUDP_DISCOVERY_PORT_LOCAL_UID
8999
9038
  ];
9000
- const broadcastHost = "255.255.255.255";
9039
+ const broadcastHosts = ["255.255.255.255"];
9040
+ const ifaces = (0, import_node_os.networkInterfaces)();
9041
+ for (const name of Object.keys(ifaces)) {
9042
+ const entries = ifaces[name];
9043
+ if (!entries) continue;
9044
+ for (const addr2 of entries) {
9045
+ if (addr2.family === "IPv4" && !addr2.internal && addr2.cidr) {
9046
+ const ipParts = addr2.address.split(".").map(Number);
9047
+ const maskParts = addr2.netmask.split(".").map(Number);
9048
+ if (ipParts.length === 4 && maskParts.length === 4) {
9049
+ const bcast = ipParts.map((octet, i) => octet | ~maskParts[i] & 255).join(".");
9050
+ if (!broadcastHosts.includes(bcast)) {
9051
+ broadcastHosts.push(bcast);
9052
+ }
9053
+ }
9054
+ }
9055
+ }
9056
+ }
9001
9057
  const directHost = (this.opts.directHost ?? "").trim();
9002
9058
  const localMode = opts?.localMode ?? "local-broadcast";
9003
9059
  const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
@@ -9024,6 +9080,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9024
9080
  )
9025
9081
  );
9026
9082
  }, discoveryTimeout);
9083
+ this.discoveryTimers.push(timeout);
9027
9084
  let retryTimer;
9028
9085
  let retryCount = 0;
9029
9086
  let discoveredSid;
@@ -9200,11 +9257,11 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9200
9257
  if (directHost) {
9201
9258
  if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
9202
9259
  return [directHost];
9203
- return [directHost, broadcastHost];
9260
+ return [directHost, ...broadcastHosts];
9204
9261
  }
9205
- return [broadcastHost];
9262
+ return broadcastHosts;
9206
9263
  }
9207
- return [broadcastHost];
9264
+ return broadcastHosts;
9208
9265
  })();
9209
9266
  for (const host of Array.from(new Set(hosts))) {
9210
9267
  for (const port of ports) {
@@ -9212,8 +9269,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9212
9269
  sock.send(packet, port, host);
9213
9270
  retryCount++;
9214
9271
  this.emit("debug", "discovery_send", { retryCount, host, port });
9215
- } catch (e) {
9216
- this.emit("error", e instanceof Error ? e : new Error(String(e)));
9272
+ } catch {
9217
9273
  }
9218
9274
  }
9219
9275
  }
@@ -9222,6 +9278,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9222
9278
  retryTimer = (0, import_node_timers.setInterval)(() => {
9223
9279
  sendDiscovery();
9224
9280
  }, retryInterval);
9281
+ this.discoveryTimers.push(retryTimer);
9225
9282
  });
9226
9283
  this.clientId = reply.cid;
9227
9284
  this.cameraId = reply.did;
@@ -9538,6 +9595,10 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9538
9595
  this.ackTimer = void 0;
9539
9596
  this.resendTimer = void 0;
9540
9597
  this.hbTimer = void 0;
9598
+ for (const t of this.discoveryTimers) {
9599
+ clearInterval(t);
9600
+ }
9601
+ this.discoveryTimers = [];
9541
9602
  const s = this.sock;
9542
9603
  this.sock = void 0;
9543
9604
  if (!s) return;
@@ -18654,6 +18715,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18654
18715
  const prefix = basename.substring(0, 10).toUpperCase();
18655
18716
  return prefix.includes("S") ? "subStream" : "mainStream";
18656
18717
  }
18718
+ /**
18719
+ * Stream profiles that the device explicitly rejected (response_code 400).
18720
+ * Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
18721
+ * it is excluded from `buildVideoStreamOptions()` results and no further
18722
+ * start attempts are made until the API instance is recreated.
18723
+ */
18724
+ _rejectedStreamProfiles = /* @__PURE__ */ new Set();
18725
+ /**
18726
+ * Check whether a stream profile was rejected by the device at runtime
18727
+ * (e.g. ext returned response_code 400).
18728
+ */
18729
+ isStreamProfileRejected(channel, profile) {
18730
+ return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
18731
+ }
18657
18732
  /**
18658
18733
  * Cache for buildVideoStreamOptions.
18659
18734
  *
@@ -23778,6 +23853,16 @@ ${stderr}`)
23778
23853
  }
23779
23854
  if (!frame) frame = await targetClient.sendFrame(baseParams);
23780
23855
  if (frame.header.responseCode !== 200) {
23856
+ if (frame.header.responseCode === 400) {
23857
+ const rejKey = `${ch}:${profile}`;
23858
+ if (!this._rejectedStreamProfiles.has(rejKey)) {
23859
+ this._rejectedStreamProfiles.add(rejKey);
23860
+ this.videoStreamOptionsCache.clear();
23861
+ this.logger?.warn?.(
23862
+ `[ReolinkBaichuanApi] Stream profile rejected by device: channel=${ch} profile=${profile} (response_code 400). This profile will be excluded from available streams. The camera may not support this stream profile with the current firmware.`
23863
+ );
23864
+ }
23865
+ }
23781
23866
  throw new Error(
23782
23867
  `Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
23783
23868
  );
@@ -25799,6 +25884,8 @@ ${xml}`
25799
25884
  for (const metadata of params.metadatas) {
25800
25885
  const profile = metadata.profile;
25801
25886
  if (isMultiFocal && profile === "ext") continue;
25887
+ if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
25888
+ continue;
25802
25889
  if (params.includeRtsp && profile !== "ext") {
25803
25890
  const streamName = profile === "main" ? "main" : "sub";
25804
25891
  pushRtsp({
@@ -32289,6 +32376,11 @@ async function createRfc4571TcpServerInternal(options) {
32289
32376
  "videoAccessUnit",
32290
32377
  onAu
32291
32378
  );
32379
+ const pendingErr = videoStream.consumePendingStartupError?.();
32380
+ if (pendingErr) {
32381
+ cleanup();
32382
+ reject(pendingErr);
32383
+ }
32292
32384
  });
32293
32385
  }
32294
32386
  };
@@ -32300,24 +32392,32 @@ async function createRfc4571TcpServerInternal(options) {
32300
32392
  await videoStream.stop();
32301
32393
  } catch {
32302
32394
  }
32303
- if (closeApiOnTeardown) {
32304
- await Promise.allSettled(
32305
- Array.from(apisToClose).map(async (a) => {
32395
+ if (dedicatedSession) {
32396
+ try {
32397
+ await dedicatedSession.release();
32398
+ } catch {
32399
+ }
32400
+ }
32401
+ if (!dedicatedSession) {
32402
+ if (closeApiOnTeardown) {
32403
+ await Promise.allSettled(
32404
+ Array.from(apisToClose).map(async (a) => {
32405
+ try {
32406
+ await a.close();
32407
+ } catch {
32408
+ }
32409
+ })
32410
+ );
32411
+ } else {
32412
+ const graceMs = isComposite ? 5e3 : 0;
32413
+ for (const a of Array.from(apisToClose)) {
32306
32414
  try {
32307
- await a.close();
32415
+ a?.client?.requestIdleDisconnectSoon?.(
32416
+ "rfc4571_teardown",
32417
+ graceMs
32418
+ );
32308
32419
  } catch {
32309
32420
  }
32310
- })
32311
- );
32312
- } else {
32313
- const graceMs = isComposite ? 5e3 : 0;
32314
- for (const a of Array.from(apisToClose)) {
32315
- try {
32316
- a?.client?.requestIdleDisconnectSoon?.(
32317
- "rfc4571_teardown",
32318
- graceMs
32319
- );
32320
- } catch {
32321
32421
  }
32322
32422
  }
32323
32423
  }
@@ -32573,7 +32673,7 @@ async function createRfc4571TcpServerInternal(options) {
32573
32673
  } catch {
32574
32674
  }
32575
32675
  }
32576
- if (closeApiOnTeardown) {
32676
+ if (closeApiOnTeardown && !dedicatedSession) {
32577
32677
  await Promise.allSettled(
32578
32678
  Array.from(apisToClose).map(async (a) => {
32579
32679
  try {