@apocaliss92/nodelink-js 0.2.5 → 0.3.5

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.
@@ -1822,6 +1822,22 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1822
1822
  static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
1823
1823
  static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
1824
1824
  static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
1825
+ /**
1826
+ * Per-client snapshot (cmd_id=109) serialization queue.
1827
+ *
1828
+ * WHY: On NVR/multi-camera devices sharing one socket, concurrent snapshot requests
1829
+ * can cause JPEG data to mix (even with per-request msgNum filtering):
1830
+ * - Camera A and B both send frames on same socket
1831
+ * - Frame listener is global per socket
1832
+ * - Timing quirks can cause chunk reordering or listener confusion
1833
+ *
1834
+ * FIX: Serialize all cmd_id=109 requests on THIS client instance.
1835
+ * Each snapshot waits for previous one to complete before starting.
1836
+ * This ensures clean frame sequences per request, zero data corruption.
1837
+ *
1838
+ * Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
1839
+ */
1840
+ snapshotQueueTail = Promise.resolve();
1825
1841
  opts;
1826
1842
  debugCfg;
1827
1843
  logger;
@@ -4303,6 +4319,20 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
4303
4319
  });
4304
4320
  }
4305
4321
  async sendBinarySnapshot109(params) {
4322
+ const prevTail = this.snapshotQueueTail;
4323
+ let resolve;
4324
+ const newTail = new Promise((r) => {
4325
+ resolve = r;
4326
+ });
4327
+ this.snapshotQueueTail = newTail;
4328
+ try {
4329
+ await prevTail;
4330
+ return await this.sendBinarySnapshot109Impl(params);
4331
+ } finally {
4332
+ resolve();
4333
+ }
4334
+ }
4335
+ async sendBinarySnapshot109Impl(params) {
4306
4336
  await this.connect();
4307
4337
  const channel = params.channel ?? this.opts.channel ?? 0;
4308
4338
  const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
@@ -4362,7 +4392,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
4362
4392
  };
4363
4393
  const onFrame = (frame) => {
4364
4394
  if (frame.header.cmdId !== cmdId) return;
4365
- if (frame.header.msgNum === msgNum && frame.header.responseCode >= 400) {
4395
+ if (frame.header.msgNum !== msgNum) return;
4396
+ if (frame.header.responseCode >= 400) {
4366
4397
  fail(
4367
4398
  new Error(
4368
4399
  `Baichuan snapshot request rejected (cmdId=${cmdId} msgNum=${msgNum} responseCode=${frame.header.responseCode})`
@@ -5928,14 +5959,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5928
5959
  `;
5929
5960
  }
5930
5961
  if (body) {
5931
- response += `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
5962
+ const bodyBuf = Buffer.from(body, "utf8");
5963
+ response += `Content-Length: ${bodyBuf.length}\r
5932
5964
  `;
5965
+ response += "\r\n";
5966
+ socket.write(response);
5967
+ socket.write(bodyBuf);
5968
+ } else {
5969
+ response += "\r\n";
5970
+ socket.write(response);
5933
5971
  }
5934
- response += "\r\n";
5935
- if (body) {
5936
- response += body;
5937
- }
5938
- socket.write(response);
5939
5972
  };
5940
5973
  this.rtspDebugLog(`RTSP ${method} ${url}`);
5941
5974
  if (this.requireAuth) {
@@ -6145,10 +6178,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6145
6178
  );
6146
6179
  }
6147
6180
  }
6148
- sendResponse(200, "OK", {
6149
- Session: sessionId,
6150
- Range: "npt=0.000-"
6151
- });
6181
+ {
6182
+ const baseUrl = `rtsp://${this.listenHost}:${this.listenPort}${this.path}`;
6183
+ const resources = this.clientResources.get(clientId);
6184
+ const rtpInfoParts = [];
6185
+ if (resources?.setupTrack0) {
6186
+ rtpInfoParts.push(`url=${baseUrl}/track0`);
6187
+ }
6188
+ if (resources?.setupTrack1) {
6189
+ rtpInfoParts.push(`url=${baseUrl}/track1`);
6190
+ }
6191
+ const playHeaders = {
6192
+ Session: sessionId,
6193
+ Range: "npt=now-"
6194
+ };
6195
+ if (rtpInfoParts.length > 0) {
6196
+ playHeaders["RTP-Info"] = rtpInfoParts.join(",");
6197
+ }
6198
+ sendResponse(200, "OK", playHeaders);
6199
+ }
6152
6200
  } else if (method === "TEARDOWN") {
6153
6201
  this.logger.info(
6154
6202
  `[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
@@ -6178,6 +6226,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6178
6226
  sdp += `c=IN IP4 ${this.listenHost}\r
6179
6227
  `;
6180
6228
  sdp += "t=0 0\r\n";
6229
+ sdp += "a=range:npt=now-\r\n";
6230
+ sdp += "a=control:*\r\n";
6181
6231
  sdp += `m=video 0 RTP/AVP ${videoPayloadType}\r
6182
6232
  `;
6183
6233
  sdp += `a=rtpmap:${videoPayloadType} ${codec}/90000\r
@@ -7120,7 +7170,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7120
7170
  this.firstFramePromise = null;
7121
7171
  this.firstFrameResolve = null;
7122
7172
  this.nativeFanout = null;
7123
- this.prebuffer = [];
7173
+ for (const [, resources] of this.clientResources) {
7174
+ const res = resources;
7175
+ res.rtpVideoBaseMicroseconds = void 0;
7176
+ res.rtpVideoBaseTimestamp = void 0;
7177
+ res.rtpVideoLastTimestamp = void 0;
7178
+ res.seenFirstVideoKeyframe = false;
7179
+ res.rtpSentVideoConfig = false;
7180
+ }
7124
7181
  if (this.dedicatedSessionRelease) {
7125
7182
  const release = this.dedicatedSessionRelease;
7126
7183
  this.dedicatedSessionRelease = void 0;
@@ -15084,13 +15141,13 @@ ${stderr}`)
15084
15141
  */
15085
15142
  async muxToMp4(params) {
15086
15143
  const { spawn: spawn3 } = await import("child_process");
15087
- const { randomUUID: randomUUID2 } = await import("crypto");
15144
+ const { randomUUID: randomUUID3 } = await import("crypto");
15088
15145
  const fs = await import("fs/promises");
15089
15146
  const os = await import("os");
15090
15147
  const path = await import("path");
15091
15148
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
15092
15149
  const tmpDir = os.tmpdir();
15093
- const id = randomUUID2();
15150
+ const id = randomUUID3();
15094
15151
  const videoFormat = params.videoCodec === "H265" ? "hevc" : "h264";
15095
15152
  const videoPath = path.join(tmpDir, `reolink-${id}.${videoFormat}`);
15096
15153
  const outputPath = path.join(tmpDir, `reolink-${id}.mp4`);
@@ -20069,8 +20126,13 @@ ${scheduleItems}
20069
20126
  };
20070
20127
 
20071
20128
  // src/reolink/discovery.ts
20129
+ import { execFile } from "child_process";
20130
+ import { randomUUID as randomUUID2 } from "crypto";
20072
20131
  import dgram3 from "dgram";
20073
- import { networkInterfaces as networkInterfaces2 } from "os";
20132
+ import * as net3 from "net";
20133
+ import { networkInterfaces as networkInterfaces2, platform } from "os";
20134
+ import { promisify } from "util";
20135
+ var execFileAsync = promisify(execFile);
20074
20136
  async function discoverViaUdpDirect(host, options) {
20075
20137
  if (!options.enableUdpDiscovery) return [];
20076
20138
  const logger = options.logger;
@@ -20432,6 +20494,348 @@ async function discoverViaUdpBroadcast(options) {
20432
20494
  });
20433
20495
  });
20434
20496
  }
20497
+ var REOLINK_MAC_PREFIXES = [
20498
+ "EC:71:DB",
20499
+ // Most common Reolink OUI
20500
+ "2C:1B:3A",
20501
+ // WiFi cameras (E1 Zoom, etc.)
20502
+ "18:2C:65",
20503
+ // Battery cameras (Video Doorbell, Argus, etc.)
20504
+ "DC:E5:37",
20505
+ // Some newer models
20506
+ "9C:8E:CD",
20507
+ // Some WiFi models
20508
+ "B4:4B:D6",
20509
+ // Some models
20510
+ "E4:3D:1A"
20511
+ // Some models
20512
+ ];
20513
+ async function discoverViaArpTable(options) {
20514
+ if (!options.enableArpLookup) return [];
20515
+ const logger = options.logger;
20516
+ logger?.log?.("[Discovery] Starting ARP table lookup for Reolink MAC prefix...");
20517
+ const discovered = [];
20518
+ try {
20519
+ let entries = [];
20520
+ if (platform() === "linux") {
20521
+ try {
20522
+ const { readFile } = await import("fs/promises");
20523
+ const content = await readFile("/proc/net/arp", "utf8");
20524
+ for (const line of content.split("\n").slice(1)) {
20525
+ const parts = line.trim().split(/\s+/);
20526
+ if (parts.length >= 4 && parts[0] && parts[3] && parts[3] !== "00:00:00:00:00:00") {
20527
+ entries.push({ ip: parts[0], mac: parts[3].toUpperCase() });
20528
+ }
20529
+ }
20530
+ } catch {
20531
+ const { stdout } = await runArpCommand();
20532
+ entries = parseArpOutput(stdout);
20533
+ }
20534
+ } else {
20535
+ const { stdout } = await runArpCommand();
20536
+ entries = parseArpOutput(stdout);
20537
+ }
20538
+ logger?.log?.(`[Discovery] ARP table has ${entries.length} entries`);
20539
+ for (const { ip, mac } of entries) {
20540
+ const isReolink = REOLINK_MAC_PREFIXES.some(
20541
+ (prefix) => mac.startsWith(prefix)
20542
+ );
20543
+ if (isReolink) {
20544
+ logger?.log?.(`[Discovery] Found Reolink device via ARP: ${ip} (MAC: ${mac})`);
20545
+ discovered.push({
20546
+ host: ip,
20547
+ discoveryMethod: "arp"
20548
+ });
20549
+ }
20550
+ }
20551
+ } catch (err) {
20552
+ const msg = err instanceof Error ? err.message : String(err);
20553
+ logger?.warn?.(`[Discovery] ARP table lookup failed: ${msg}`);
20554
+ }
20555
+ logger?.log?.(`[Discovery] ARP lookup complete. Found ${discovered.length} device(s).`);
20556
+ return discovered;
20557
+ }
20558
+ async function runArpCommand() {
20559
+ const paths = ["/usr/sbin/arp", "/sbin/arp", "/usr/bin/arp", "arp"];
20560
+ for (const arpPath of paths) {
20561
+ try {
20562
+ return await execFileAsync(arpPath, ["-an"], { timeout: 5e3 });
20563
+ } catch {
20564
+ }
20565
+ }
20566
+ throw new Error("arp command not found");
20567
+ }
20568
+ function parseArpOutput(stdout) {
20569
+ const results = [];
20570
+ for (const line of stdout.split("\n")) {
20571
+ const match = /\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)/i.exec(line);
20572
+ if (match && match[1] && match[2] && match[2] !== "(incomplete)") {
20573
+ results.push({ ip: match[1], mac: match[2].toUpperCase() });
20574
+ }
20575
+ }
20576
+ return results;
20577
+ }
20578
+ async function discoverViaDhcpListener(options) {
20579
+ if (!options.enableDhcpListener) return [];
20580
+ const logger = options.logger;
20581
+ const timeoutMs = options.dhcpListenerTimeoutMs ?? 1e4;
20582
+ logger?.log?.(`[Discovery] Starting passive DHCP listener (${timeoutMs}ms)...`);
20583
+ const discovered = /* @__PURE__ */ new Map();
20584
+ return new Promise((resolve) => {
20585
+ let socket;
20586
+ let timeout;
20587
+ try {
20588
+ socket = dgram3.createSocket({ type: "udp4", reuseAddr: true });
20589
+ } catch (err) {
20590
+ logger?.warn?.(`[Discovery] DHCP: failed to create socket: ${err instanceof Error ? err.message : String(err)}`);
20591
+ resolve([]);
20592
+ return;
20593
+ }
20594
+ socket.on("message", (msg) => {
20595
+ try {
20596
+ if (msg.length < 240) return;
20597
+ const op = msg[0];
20598
+ const hlen = msg[2];
20599
+ if (hlen !== 6) return;
20600
+ const mac = [
20601
+ msg[28]?.toString(16).padStart(2, "0"),
20602
+ msg[29]?.toString(16).padStart(2, "0"),
20603
+ msg[30]?.toString(16).padStart(2, "0"),
20604
+ msg[31]?.toString(16).padStart(2, "0"),
20605
+ msg[32]?.toString(16).padStart(2, "0"),
20606
+ msg[33]?.toString(16).padStart(2, "0")
20607
+ ].join(":").toUpperCase();
20608
+ const isReolinkMac = REOLINK_MAC_PREFIXES.some((p) => mac.startsWith(p));
20609
+ let hostname = "";
20610
+ let i = 240;
20611
+ while (i < msg.length - 1) {
20612
+ const optType = msg[i];
20613
+ if (optType === 255) break;
20614
+ if (optType === 0) {
20615
+ i++;
20616
+ continue;
20617
+ }
20618
+ const optLen = msg[i + 1] ?? 0;
20619
+ if (optType === 12 && optLen > 0) {
20620
+ hostname = msg.subarray(i + 2, i + 2 + optLen).toString("ascii").toLowerCase();
20621
+ }
20622
+ i += 2 + optLen;
20623
+ }
20624
+ const isReolinkHostname = hostname.startsWith("reolink");
20625
+ if (!isReolinkMac && !isReolinkHostname) return;
20626
+ const yiaddr = `${msg[16]}.${msg[17]}.${msg[18]}.${msg[19]}`;
20627
+ const ciaddr = `${msg[12]}.${msg[13]}.${msg[14]}.${msg[15]}`;
20628
+ const ip = yiaddr !== "0.0.0.0" ? yiaddr : ciaddr;
20629
+ if (ip === "0.0.0.0" || !ip) return;
20630
+ if (!discovered.has(ip)) {
20631
+ logger?.log?.(`[Discovery] DHCP: found Reolink device ${ip} (MAC: ${mac}, hostname: ${hostname || "n/a"}, op: ${op === 1 ? "request" : "reply"})`);
20632
+ const device = {
20633
+ host: ip,
20634
+ discoveryMethod: "dhcp"
20635
+ };
20636
+ if (hostname) device.name = hostname;
20637
+ discovered.set(ip, device);
20638
+ }
20639
+ } catch {
20640
+ }
20641
+ });
20642
+ socket.on("error", (err) => {
20643
+ logger?.warn?.(`[Discovery] DHCP socket error: ${err.message}`);
20644
+ clearTimeout(timeout);
20645
+ socket.close();
20646
+ resolve(Array.from(discovered.values()));
20647
+ });
20648
+ socket.bind(67, "0.0.0.0", () => {
20649
+ logger?.log?.("[Discovery] DHCP listener bound on port 67");
20650
+ timeout = setTimeout(() => {
20651
+ socket.close();
20652
+ logger?.log?.(`[Discovery] DHCP listener complete. Found ${discovered.size} device(s).`);
20653
+ resolve(Array.from(discovered.values()));
20654
+ }, timeoutMs);
20655
+ });
20656
+ });
20657
+ }
20658
+ function probeTcpPort(ip, port, timeoutMs) {
20659
+ return new Promise((resolve) => {
20660
+ const socket = new net3.Socket();
20661
+ let settled = false;
20662
+ const done = (result) => {
20663
+ if (settled) return;
20664
+ settled = true;
20665
+ socket.destroy();
20666
+ resolve(result);
20667
+ };
20668
+ socket.setTimeout(timeoutMs);
20669
+ socket.on("connect", () => done(true));
20670
+ socket.on("timeout", () => done(false));
20671
+ socket.on("error", () => done(false));
20672
+ socket.connect(port, ip);
20673
+ });
20674
+ }
20675
+ async function discoverViaTcpPortScan(options) {
20676
+ if (!options.enableTcpPortScan) return [];
20677
+ const logger = options.logger;
20678
+ const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
20679
+ const timeoutMs = options.tcpProbeTimeoutMs ?? 1500;
20680
+ const maxConcurrent = options.maxConcurrentProbes ?? 80;
20681
+ if (!networkCidr) {
20682
+ logger?.warn?.("[Discovery] No network CIDR available for TCP port scan");
20683
+ return [];
20684
+ }
20685
+ logger?.log?.(`[Discovery] Starting TCP port 9000 scan on network ${networkCidr}...`);
20686
+ const ipRange = parseCidr(networkCidr);
20687
+ if (!ipRange) {
20688
+ logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
20689
+ return [];
20690
+ }
20691
+ const discovered = [];
20692
+ const ipAddresses = [];
20693
+ for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) {
20694
+ ipAddresses.push(ipNumberToString(ipNum));
20695
+ }
20696
+ logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IPs on port 9000...`);
20697
+ for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
20698
+ const batch = ipAddresses.slice(i, i + maxConcurrent);
20699
+ const batchResults = await Promise.allSettled(
20700
+ batch.map(async (ip) => {
20701
+ const open = await probeTcpPort(ip, 9e3, timeoutMs);
20702
+ if (open) {
20703
+ logger?.log?.(`[Discovery] Found Baichuan device at ${ip}:9000`);
20704
+ return { host: ip, discoveryMethod: "tcp_port_scan" };
20705
+ }
20706
+ return null;
20707
+ })
20708
+ );
20709
+ for (const result of batchResults) {
20710
+ if (result.status === "fulfilled" && result.value) {
20711
+ discovered.push(result.value);
20712
+ }
20713
+ }
20714
+ }
20715
+ logger?.log?.(`[Discovery] TCP port scan complete. Found ${discovered.length} device(s).`);
20716
+ return discovered;
20717
+ }
20718
+ async function discoverViaOnvif(options) {
20719
+ if (!options.enableOnvifDiscovery) return [];
20720
+ const logger = options.logger;
20721
+ const timeoutMs = options.onvifDiscoveryTimeoutMs ?? 5e3;
20722
+ logger?.log?.(`[Discovery] Starting ONVIF WS-Discovery (${timeoutMs}ms)...`);
20723
+ const discovered = /* @__PURE__ */ new Map();
20724
+ const MULTICAST_ADDR = "239.255.255.250";
20725
+ const MULTICAST_PORT = 3702;
20726
+ const messageId = `uuid:${randomUUID2()}`;
20727
+ const probeMessage = [
20728
+ '<?xml version="1.0" encoding="UTF-8"?>',
20729
+ '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"',
20730
+ ' xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"',
20731
+ ' xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"',
20732
+ ' xmlns:dn="http://www.onvif.org/ver10/network/wsdl">',
20733
+ " <s:Header>",
20734
+ ` <a:MessageID>${messageId}</a:MessageID>`,
20735
+ " <a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>",
20736
+ " <a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>",
20737
+ " </s:Header>",
20738
+ " <s:Body>",
20739
+ " <d:Probe>",
20740
+ " <d:Types>dn:NetworkVideoTransmitter</d:Types>",
20741
+ " </d:Probe>",
20742
+ " </s:Body>",
20743
+ "</s:Envelope>"
20744
+ ].join("\n");
20745
+ return new Promise((resolve) => {
20746
+ const socket = dgram3.createSocket({ type: "udp4", reuseAddr: true });
20747
+ let timeout;
20748
+ socket.on("message", (msg, rinfo) => {
20749
+ try {
20750
+ const xml = msg.toString("utf8");
20751
+ const xaddrsMatch = /<[^:]*:?XAddrs>([^<]+)<\/[^:]*:?XAddrs>/i.exec(xml);
20752
+ const scopesMatch = /<[^:]*:?Scopes>([^<]+)<\/[^:]*:?Scopes>/i.exec(xml);
20753
+ let host = rinfo.address;
20754
+ let httpPort;
20755
+ if (xaddrsMatch?.[1]) {
20756
+ const urls = xaddrsMatch[1].trim().split(/\s+/);
20757
+ for (const url of urls) {
20758
+ try {
20759
+ const parsed = new URL(url);
20760
+ if (parsed.hostname) {
20761
+ host = parsed.hostname;
20762
+ const p = Number.parseInt(parsed.port, 10);
20763
+ if (p && p !== 80) httpPort = p;
20764
+ break;
20765
+ }
20766
+ } catch {
20767
+ }
20768
+ }
20769
+ }
20770
+ if (discovered.has(host)) return;
20771
+ let model;
20772
+ let name;
20773
+ let manufacturer;
20774
+ if (scopesMatch?.[1]) {
20775
+ const scopes = scopesMatch[1].trim().split(/\s+/);
20776
+ for (const scope of scopes) {
20777
+ const hwMatch = /\/hardware\/(.+)$/i.exec(scope);
20778
+ if (hwMatch?.[1]) model = decodeURIComponent(hwMatch[1]);
20779
+ const nameMatch = /\/name\/(.+)$/i.exec(scope);
20780
+ if (nameMatch?.[1]) name = decodeURIComponent(nameMatch[1]);
20781
+ const mfgMatch = /\/manufacturer\/(.+)$/i.exec(scope);
20782
+ if (mfgMatch?.[1]) manufacturer = decodeURIComponent(mfgMatch[1]);
20783
+ }
20784
+ }
20785
+ const allText = `${manufacturer ?? ""} ${model ?? ""} ${xaddrsMatch?.[1] ?? ""}`.toLowerCase();
20786
+ const hasReolinkText = allText.includes("reolink");
20787
+ const hasReolinkModel = /^(rlc|rln|rl[ncb]|e1|cw|cx|duo|trackmix|argus|lumus|go|video doorbell|reolink)/i.test(model ?? "");
20788
+ const isReolink = hasReolinkText || hasReolinkModel;
20789
+ if (!isReolink) {
20790
+ logger?.debug?.(`[Discovery] ONVIF: skipping non-Reolink device at ${host} (${model ?? "unknown"}, manufacturer: ${manufacturer ?? "unknown"})`);
20791
+ return;
20792
+ }
20793
+ logger?.log?.(`[Discovery] ONVIF: found Reolink device at ${host}${model ? ` (${model})` : ""}${name ? ` name="${name}"` : ""}`);
20794
+ const device = {
20795
+ host,
20796
+ discoveryMethod: "onvif"
20797
+ };
20798
+ if (model) device.model = model;
20799
+ if (name && name !== "IPC") {
20800
+ device.name = name;
20801
+ } else if (model) {
20802
+ device.name = model;
20803
+ }
20804
+ if (httpPort) device.httpPort = httpPort;
20805
+ discovered.set(host, device);
20806
+ } catch {
20807
+ }
20808
+ });
20809
+ socket.on("error", (err) => {
20810
+ logger?.warn?.(`[Discovery] ONVIF socket error: ${err.message}`);
20811
+ });
20812
+ socket.bind(0, "0.0.0.0", () => {
20813
+ const buf = Buffer.from(probeMessage, "utf8");
20814
+ socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
20815
+ if (err) {
20816
+ logger?.warn?.(`[Discovery] ONVIF: failed to send probe: ${err.message}`);
20817
+ }
20818
+ });
20819
+ setTimeout(() => {
20820
+ try {
20821
+ socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR);
20822
+ } catch {
20823
+ }
20824
+ }, 500);
20825
+ timeout = setTimeout(() => {
20826
+ try {
20827
+ socket.close();
20828
+ } catch {
20829
+ }
20830
+ logger?.log?.(`[Discovery] ONVIF WS-Discovery complete. Found ${discovered.size} device(s).`);
20831
+ resolve(Array.from(discovered.values()));
20832
+ }, timeoutMs);
20833
+ });
20834
+ socket.on("close", () => {
20835
+ if (timeout) clearTimeout(timeout);
20836
+ });
20837
+ });
20838
+ }
20435
20839
  async function discoverReolinkDevices(options = {}) {
20436
20840
  const logger = options.logger;
20437
20841
  logger?.log?.("[Discovery] Starting Reolink device discovery...");
@@ -20454,10 +20858,26 @@ async function discoverReolinkDevices(options = {}) {
20454
20858
  results.push(seenDevices.get(key));
20455
20859
  }
20456
20860
  };
20457
- const [httpDevices, udpDevices] = await Promise.all([
20861
+ const [httpDevices, udpDevices, tcpDevices, arpDevices, dhcpDevices, onvifDevices] = await Promise.all([
20458
20862
  discoverViaHttpScan(options),
20459
- discoverViaUdpBroadcast(options)
20863
+ discoverViaUdpBroadcast(options),
20864
+ discoverViaTcpPortScan(options),
20865
+ discoverViaArpTable(options),
20866
+ discoverViaDhcpListener(options),
20867
+ discoverViaOnvif(options)
20460
20868
  ]);
20869
+ for (const device of dhcpDevices) {
20870
+ mergeDevice(device);
20871
+ }
20872
+ for (const device of arpDevices) {
20873
+ mergeDevice(device);
20874
+ }
20875
+ for (const device of tcpDevices) {
20876
+ mergeDevice(device);
20877
+ }
20878
+ for (const device of onvifDevices) {
20879
+ mergeDevice(device);
20880
+ }
20461
20881
  for (const device of httpDevices) {
20462
20882
  mergeDevice(device);
20463
20883
  }
@@ -20534,8 +20954,8 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
20534
20954
  async function pingHost(host, timeoutMs = 3e3) {
20535
20955
  return new Promise((resolve) => {
20536
20956
  const { exec } = __require("child_process");
20537
- const platform = process.platform;
20538
- const pingCmd = platform === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform === "darwin" ? (
20957
+ const platform2 = process.platform;
20958
+ const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
20539
20959
  // macOS: -W is in milliseconds (Linux: seconds)
20540
20960
  `ping -c 1 -W ${timeoutMs} ${host}`
20541
20961
  ) : (
@@ -21076,10 +21496,14 @@ export {
21076
21496
  discoverViaUdpDirect,
21077
21497
  discoverViaHttpScan,
21078
21498
  discoverViaUdpBroadcast,
21499
+ discoverViaArpTable,
21500
+ discoverViaDhcpListener,
21501
+ discoverViaTcpPortScan,
21502
+ discoverViaOnvif,
21079
21503
  discoverReolinkDevices,
21080
21504
  normalizeUid,
21081
21505
  maskUid,
21082
21506
  isTcpFailureThatShouldFallbackToUdp,
21083
21507
  autoDetectDeviceType
21084
21508
  };
21085
- //# sourceMappingURL=chunk-EG5IY3CM.js.map
21509
+ //# sourceMappingURL=chunk-UDS2UR4S.js.map