@apocaliss92/nodelink-js 0.5.2 → 0.6.1

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,7 +3,7 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-7HSTETZR.js";
6
+ } from "../chunk-EAHRVNEX.js";
7
7
  import "../chunk-XDVBNZGR.js";
8
8
  import {
9
9
  __require
package/dist/index.cjs CHANGED
@@ -9047,12 +9047,28 @@ async function getServerBinding(uid, options = {}) {
9047
9047
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
9048
9048
  const logger = options.logger;
9049
9049
  if (typeof fetchImpl !== "function") {
9050
- logger?.debug?.(
9050
+ logger?.log?.(
9051
9051
  `[server-binding] global fetch unavailable; skipping cloud lookup`
9052
9052
  );
9053
9053
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9054
9054
  return void 0;
9055
9055
  }
9056
+ try {
9057
+ const apiHostname = new URL(baseUrl).hostname;
9058
+ const dns2 = await import("dns/promises");
9059
+ const answers = await dns2.lookup(apiHostname, { family: 4, all: true });
9060
+ const sinkholed = answers.find(
9061
+ (a) => a.address?.startsWith("127.") || a.address === "0.0.0.0" || a.address?.startsWith("10.") || a.address?.startsWith("192.168.") || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(a.address ?? "")
9062
+ );
9063
+ if (sinkholed) {
9064
+ logger?.log?.(
9065
+ `[server-binding] ${uid}: DNS for ${apiHostname} resolves to ${sinkholed.address} (sinkhole / /etc/hosts override). Cloud directory unreachable \u2014 falling back to the 22-hostname P2P sweep. Whitelist ${apiHostname} to enable.`
9066
+ );
9067
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9068
+ return void 0;
9069
+ }
9070
+ } catch {
9071
+ }
9056
9072
  const url = `${baseUrl}/devices/${encodeURIComponent(uid)}/server-binding?language=${encodeURIComponent(language)}`;
9057
9073
  const controller = new AbortController();
9058
9074
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -9063,8 +9079,8 @@ async function getServerBinding(uid, options = {}) {
9063
9079
  headers: { Accept: "application/json" }
9064
9080
  });
9065
9081
  if (!res.ok) {
9066
- logger?.debug?.(
9067
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText}`
9082
+ logger?.log?.(
9083
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
9068
9084
  );
9069
9085
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9070
9086
  return void 0;
@@ -9072,8 +9088,15 @@ async function getServerBinding(uid, options = {}) {
9072
9088
  const json = await res.json();
9073
9089
  const parsed = parseServerBindingResponse(json);
9074
9090
  if (!parsed) {
9075
- logger?.debug?.(
9076
- `[server-binding] ${uid}: response shape did not match expectations`
9091
+ logger?.log?.(
9092
+ `[server-binding] ${uid}: response shape did not match expectations (Reolink schema change?)`
9093
+ );
9094
+ cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9095
+ return void 0;
9096
+ }
9097
+ if (parsed.availableZones.length === 0) {
9098
+ logger?.log?.(
9099
+ `[server-binding] ${uid}: cloud returned 0 zones \u2014 UID not registered with Reolink cloud (or wrong region)`
9077
9100
  );
9078
9101
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9079
9102
  return void 0;
@@ -9092,9 +9115,23 @@ async function getServerBinding(uid, options = {}) {
9092
9115
  );
9093
9116
  return parsed;
9094
9117
  } catch (e) {
9095
- logger?.debug?.(
9096
- `[server-binding] ${uid}: ${e?.message ?? String(e)}`
9097
- );
9118
+ const msg = e?.message ?? String(e);
9119
+ const errName = e?.name;
9120
+ if (errName === "AbortError" || msg.includes("aborted")) {
9121
+ logger?.log?.(
9122
+ `[server-binding] ${uid}: timed out after ${timeoutMs}ms (cloud unreachable)`
9123
+ );
9124
+ } else if (msg.includes("ENOTFOUND") || msg.includes("EAI_AGAIN")) {
9125
+ logger?.log?.(
9126
+ `[server-binding] ${uid}: DNS failed (${msg}) \u2014 apis.reolink.com may be blocked at resolver`
9127
+ );
9128
+ } else if (msg.includes("ECONNREFUSED") || msg.includes("EHOSTUNREACH") || msg.includes("ENETUNREACH")) {
9129
+ logger?.log?.(
9130
+ `[server-binding] ${uid}: network unreachable (${msg}) \u2014 cloud port blocked`
9131
+ );
9132
+ } else {
9133
+ logger?.log?.(`[server-binding] ${uid}: fetch failed \u2014 ${msg}`);
9134
+ }
9098
9135
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
9099
9136
  return void 0;
9100
9137
  } finally {
@@ -9530,12 +9567,19 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9530
9567
  const tid = (Math.floor(Math.random() * 2147483647) | 0) >>> 0;
9531
9568
  const xml = buildC2mQ({ uid });
9532
9569
  const pkt = encodeDiscoveryPacket(tid, xml);
9570
+ const counters = { sentBytes: 0, rxBytes: 0 };
9533
9571
  return await new Promise((resolve, reject) => {
9534
9572
  const deadline = setTimeout(() => {
9535
9573
  cleanup();
9536
- reject(new Error(`P2P UID lookup timeout (${dest.host}:${dest.port})`));
9574
+ const err = new Error(
9575
+ `P2P UID lookup timeout (${dest.host}:${dest.port}) \u2014 sent=${counters.sentBytes}B rx=${counters.rxBytes}B`
9576
+ );
9577
+ err.sentBytes = counters.sentBytes;
9578
+ err.rxBytes = counters.rxBytes;
9579
+ reject(err);
9537
9580
  }, timeoutMs);
9538
9581
  const onMsg = (msg) => {
9582
+ counters.rxBytes += msg.length;
9539
9583
  try {
9540
9584
  const p = decodeBcUdpPacket(msg);
9541
9585
  if (p.kind !== "discovery") return;
@@ -9543,13 +9587,19 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
9543
9587
  const qr = parseM2cQr(p.xml);
9544
9588
  if (!qr?.reg || !qr?.relay) return;
9545
9589
  cleanup();
9546
- resolve({ reg: qr.reg, relay: qr.relay });
9590
+ resolve({
9591
+ reg: qr.reg,
9592
+ relay: qr.relay,
9593
+ sentBytes: counters.sentBytes,
9594
+ rxBytes: counters.rxBytes
9595
+ });
9547
9596
  } catch {
9548
9597
  }
9549
9598
  };
9550
9599
  const send = () => {
9551
9600
  try {
9552
9601
  sock.send(pkt, dest.port, dest.host);
9602
+ counters.sentBytes += pkt.length;
9553
9603
  } catch {
9554
9604
  }
9555
9605
  };
@@ -35165,10 +35215,18 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35165
35215
  }
35166
35216
  }
35167
35217
  async startFfmpegCompositionFromRtspUrls(mainWidth, mainHeight, pipWidth, pipHeight, position, widerRtspUrl, teleRtspUrl, rtspTransport) {
35218
+ const videoEncoder = this.options.videoEncoder ?? "libx264";
35219
+ const isX264 = videoEncoder === "libx264";
35220
+ const encoderPreset = this.options.encoderPreset ?? "ultrafast";
35221
+ const crf = this.options.crf ?? 23;
35222
+ const gopSeconds = this.options.gopSeconds ?? 1;
35223
+ const assumedFps = 30;
35224
+ const gopFrames = Math.max(1, Math.round(gopSeconds * assumedFps));
35168
35225
  const ffmpegArgs = [
35169
35226
  "-hide_banner",
35170
35227
  "-loglevel",
35171
35228
  "error",
35229
+ ...this.options.extraGlobalArgs ?? [],
35172
35230
  "-fflags",
35173
35231
  "+genpts",
35174
35232
  // Input 0: wider
@@ -35189,27 +35247,33 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35189
35247
  // Output: always H.264 Annex-B
35190
35248
  "-an",
35191
35249
  "-c:v",
35192
- "libx264",
35250
+ videoEncoder,
35193
35251
  "-g",
35194
- "30",
35252
+ String(gopFrames),
35195
35253
  "-keyint_min",
35196
- "30",
35254
+ String(gopFrames),
35197
35255
  "-sc_threshold",
35198
35256
  "0",
35199
- "-x264-params",
35200
- "aud=1:repeat-headers=1:keyint=30:min-keyint=30:scenecut=0",
35201
- "-preset",
35202
- "ultrafast",
35203
- "-tune",
35204
- "zerolatency",
35205
- "-crf",
35206
- "23",
35257
+ ...isX264 ? [
35258
+ "-x264-params",
35259
+ `aud=1:repeat-headers=1:keyint=${gopFrames}:min-keyint=${gopFrames}:scenecut=0`,
35260
+ "-preset",
35261
+ encoderPreset,
35262
+ "-tune",
35263
+ "zerolatency",
35264
+ "-crf",
35265
+ String(crf)
35266
+ ] : [],
35267
+ ...this.options.extraOutputArgs ?? [],
35207
35268
  "-f",
35208
35269
  "h264",
35209
35270
  "pipe:1"
35210
35271
  ];
35211
- this.logger.log?.(`[CompositeStream] Starting ffmpeg (rtsp inputs): ${ffmpegArgs.join(" ")}`);
35212
- this.ffmpegProcess = (0, import_node_child_process7.spawn)("ffmpeg", ffmpegArgs, {
35272
+ const ffmpegBin = this.options.ffmpegPath ?? "ffmpeg";
35273
+ this.logger.log?.(
35274
+ `[CompositeStream] Starting ffmpeg (rtsp inputs): bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35275
+ );
35276
+ this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
35213
35277
  stdio: ["ignore", "pipe", "pipe"]
35214
35278
  });
35215
35279
  this.ffmpegProcess.on("error", (error) => {
@@ -35276,10 +35340,18 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35276
35340
  "-i",
35277
35341
  "pipe:3"
35278
35342
  ];
35343
+ const videoEncoder = this.options.videoEncoder ?? "libx264";
35344
+ const isX264 = videoEncoder === "libx264";
35345
+ const encoderPreset = this.options.encoderPreset ?? "ultrafast";
35346
+ const crf = this.options.crf ?? 23;
35347
+ const gopSeconds = this.options.gopSeconds ?? 1;
35348
+ const assumedFps = 30;
35349
+ const gopFrames = Math.max(1, Math.round(gopSeconds * assumedFps));
35279
35350
  const ffmpegArgs = [
35280
35351
  "-hide_banner",
35281
35352
  "-loglevel",
35282
35353
  "error",
35354
+ ...this.options.extraGlobalArgs ?? [],
35283
35355
  "-fflags",
35284
35356
  "+genpts",
35285
35357
  "-probesize",
@@ -35298,33 +35370,40 @@ var CompositeStream = class extends import_node_events6.EventEmitter {
35298
35370
  "-map",
35299
35371
  "[out]",
35300
35372
  "-c:v",
35301
- "libx264",
35302
- // Re-encode for compatibility
35303
- // Make the stream easy to join mid-flight: frequent IDRs + in-band headers + AUD.
35304
- // Without this, a new client may wait many seconds for the next keyframe.
35373
+ videoEncoder,
35374
+ // Make the stream easy to join mid-flight: frequent IDRs + in-band
35375
+ // headers + AUD. Without this, a new client may wait many seconds
35376
+ // for the next keyframe.
35305
35377
  "-g",
35306
- "30",
35378
+ String(gopFrames),
35307
35379
  "-keyint_min",
35308
- "30",
35380
+ String(gopFrames),
35309
35381
  "-sc_threshold",
35310
35382
  "0",
35311
- "-x264-params",
35312
- "aud=1:repeat-headers=1:keyint=30:min-keyint=30:scenecut=0",
35313
- "-preset",
35314
- "ultrafast",
35315
- "-tune",
35316
- "zerolatency",
35317
- "-crf",
35318
- "23",
35383
+ // libx264-specific knobs. We deliberately skip these for HW encoders
35384
+ // — each one has its own option vocabulary (`-q:v`, `-rc`, etc.)
35385
+ // and the user is expected to express them via extraOutputArgs.
35386
+ ...isX264 ? [
35387
+ "-x264-params",
35388
+ `aud=1:repeat-headers=1:keyint=${gopFrames}:min-keyint=${gopFrames}:scenecut=0`,
35389
+ "-preset",
35390
+ encoderPreset,
35391
+ "-tune",
35392
+ "zerolatency",
35393
+ "-crf",
35394
+ String(crf)
35395
+ ] : [],
35396
+ ...this.options.extraOutputArgs ?? [],
35319
35397
  "-f",
35320
35398
  "h264",
35321
35399
  "pipe:1"
35322
35400
  // Output (stdout)
35323
35401
  ];
35402
+ const ffmpegBin = this.options.ffmpegPath ?? "ffmpeg";
35324
35403
  this.logger.log?.(
35325
- `[CompositeStream] Starting ffmpeg: ${ffmpegArgs.join(" ")}`
35404
+ `[CompositeStream] Starting ffmpeg: bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
35326
35405
  );
35327
- this.ffmpegProcess = (0, import_node_child_process7.spawn)("ffmpeg", ffmpegArgs, {
35406
+ this.ffmpegProcess = (0, import_node_child_process7.spawn)(ffmpegBin, ffmpegArgs, {
35328
35407
  stdio: ["pipe", "pipe", "pipe", "pipe"]
35329
35408
  });
35330
35409
  this.ffmpegProcess.on("error", (error) => {
@@ -36032,6 +36111,20 @@ async function createRfc4571TcpServerInternal(options) {
36032
36111
  ...forceH264 !== void 0 ? { forceH264 } : defaultForceH264 ? { forceH264: true } : {},
36033
36112
  ...compositeOptions?.assumeH264Inputs !== void 0 ? { assumeH264Inputs: compositeOptions.assumeH264Inputs } : {},
36034
36113
  ...compositeOptions?.disableTranscode !== void 0 ? { disableTranscode: compositeOptions.disableTranscode } : {},
36114
+ // Propagate ffmpeg binary path — required when the embedder strips
36115
+ // PATH (Scrypted on Windows, Electron sandboxes, distroless Docker)
36116
+ // and the bundled ffmpeg is at a fixed absolute path only the
36117
+ // embedder knows.
36118
+ ...compositeOptions?.ffmpegPath ? { ffmpegPath: compositeOptions.ffmpegPath } : {},
36119
+ // Encoder tuning knobs — see CompositeStreamPipOptions for the
36120
+ // semantic contract on each one. Plumbed verbatim so the
36121
+ // CompositeStream layer can apply defaults.
36122
+ ...compositeOptions?.videoEncoder ? { videoEncoder: compositeOptions.videoEncoder } : {},
36123
+ ...compositeOptions?.encoderPreset ? { encoderPreset: compositeOptions.encoderPreset } : {},
36124
+ ...typeof compositeOptions?.crf === "number" ? { crf: compositeOptions.crf } : {},
36125
+ ...typeof compositeOptions?.gopSeconds === "number" ? { gopSeconds: compositeOptions.gopSeconds } : {},
36126
+ ...compositeOptions?.extraGlobalArgs ? { extraGlobalArgs: compositeOptions.extraGlobalArgs } : {},
36127
+ ...compositeOptions?.extraOutputArgs ? { extraOutputArgs: compositeOptions.extraOutputArgs } : {},
36035
36128
  logger
36036
36129
  });
36037
36130
  isCompositeStream = true;