@apocaliss92/nodelink-js 0.2.4 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,9 +39,13 @@ The library includes a **complete web-based management interface** for easy came
39
39
  </p>
40
40
 
41
41
  - 🎛️ **Camera Management** - Add, configure, and monitor multiple cameras
42
- - 📹 **Live Streaming** - Preview streams via MJPEG, WebRTC, or RTSP
42
+ - 📡 **NVR / Hub Support** - Add NVRs as first-class entities, discover channels, and manage child cameras. All cameras on an NVR share a single connection (like Scrypted). Connect/disconnect at the NVR level; add or remove cameras at any time via channel discovery
43
+ - 🔋 **Battery Camera Support** - Cameras are auto-detected as battery-powered when they emit sleep/wake events. Per-camera battery mode setting: **Stream Only** (default — camera sleeps when no stream clients) or **Always On** (stays awake while connected). Live awake/sleeping badge on each camera card. Controls and stream discovery are paused while the camera sleeps to avoid unnecessary wake-ups
44
+ - 💡 **Camera Controls** - Toggle floodlight, siren, floodlight-on-motion, siren-on-motion, PTZ auto-tracking, and PIR sensor directly from the camera card. PTZ directional controls and preset navigation via a dedicated modal
45
+ - 📹 **Live Streaming** - Preview streams via MJPEG, WebRTC, or HLS. Stream options are cached so battery cameras show available streams even while sleeping
46
+ - 🔔 **Real-time Events** - Per-camera event viewer with live SSE updates (motion, doorbell, people, vehicle, animal, face, package, day/night, sleep/wake). Events are broadcast via SSE, NDJSON stream, and MQTT
43
47
  - 📊 **Real-time Logs** - Monitor camera events and system logs
44
- - ⚙️ **Settings** - Configure RTSP proxy, ports, and auto-start options
48
+ - ⚙️ **Settings** - Configure RTSP proxy, ports, auto-start options, MQTT broker, and Home Assistant discovery
45
49
  - 📱 **PWA Support** - Install as a Progressive Web App on mobile devices
46
50
  - 🌐 **Responsive Design** - Works on desktop, tablet, and mobile
47
51
 
@@ -5928,14 +5928,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5928
5928
  `;
5929
5929
  }
5930
5930
  if (body) {
5931
- response += `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
5931
+ const bodyBuf = Buffer.from(body, "utf8");
5932
+ response += `Content-Length: ${bodyBuf.length}\r
5932
5933
  `;
5934
+ response += "\r\n";
5935
+ socket.write(response);
5936
+ socket.write(bodyBuf);
5937
+ } else {
5938
+ response += "\r\n";
5939
+ socket.write(response);
5933
5940
  }
5934
- response += "\r\n";
5935
- if (body) {
5936
- response += body;
5937
- }
5938
- socket.write(response);
5939
5941
  };
5940
5942
  this.rtspDebugLog(`RTSP ${method} ${url}`);
5941
5943
  if (this.requireAuth) {
@@ -6145,10 +6147,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6145
6147
  );
6146
6148
  }
6147
6149
  }
6148
- sendResponse(200, "OK", {
6149
- Session: sessionId,
6150
- Range: "npt=0.000-"
6151
- });
6150
+ {
6151
+ const baseUrl = `rtsp://${this.listenHost}:${this.listenPort}${this.path}`;
6152
+ const resources = this.clientResources.get(clientId);
6153
+ const rtpInfoParts = [];
6154
+ if (resources?.setupTrack0) {
6155
+ rtpInfoParts.push(`url=${baseUrl}/track0`);
6156
+ }
6157
+ if (resources?.setupTrack1) {
6158
+ rtpInfoParts.push(`url=${baseUrl}/track1`);
6159
+ }
6160
+ const playHeaders = {
6161
+ Session: sessionId,
6162
+ Range: "npt=now-"
6163
+ };
6164
+ if (rtpInfoParts.length > 0) {
6165
+ playHeaders["RTP-Info"] = rtpInfoParts.join(",");
6166
+ }
6167
+ sendResponse(200, "OK", playHeaders);
6168
+ }
6152
6169
  } else if (method === "TEARDOWN") {
6153
6170
  this.logger.info(
6154
6171
  `[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
@@ -6178,6 +6195,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6178
6195
  sdp += `c=IN IP4 ${this.listenHost}\r
6179
6196
  `;
6180
6197
  sdp += "t=0 0\r\n";
6198
+ sdp += "a=range:npt=now-\r\n";
6199
+ sdp += "a=control:*\r\n";
6181
6200
  sdp += `m=video 0 RTP/AVP ${videoPayloadType}\r
6182
6201
  `;
6183
6202
  sdp += `a=rtpmap:${videoPayloadType} ${codec}/90000\r
@@ -7120,7 +7139,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7120
7139
  this.firstFramePromise = null;
7121
7140
  this.firstFrameResolve = null;
7122
7141
  this.nativeFanout = null;
7123
- this.prebuffer = [];
7142
+ for (const [, resources] of this.clientResources) {
7143
+ const res = resources;
7144
+ res.rtpVideoBaseMicroseconds = void 0;
7145
+ res.rtpVideoBaseTimestamp = void 0;
7146
+ res.rtpVideoLastTimestamp = void 0;
7147
+ res.seenFirstVideoKeyframe = false;
7148
+ res.rtpSentVideoConfig = false;
7149
+ }
7124
7150
  if (this.dedicatedSessionRelease) {
7125
7151
  const release = this.dedicatedSessionRelease;
7126
7152
  this.dedicatedSessionRelease = void 0;
@@ -15084,13 +15110,13 @@ ${stderr}`)
15084
15110
  */
15085
15111
  async muxToMp4(params) {
15086
15112
  const { spawn: spawn3 } = await import("child_process");
15087
- const { randomUUID: randomUUID2 } = await import("crypto");
15113
+ const { randomUUID: randomUUID3 } = await import("crypto");
15088
15114
  const fs = await import("fs/promises");
15089
15115
  const os = await import("os");
15090
15116
  const path = await import("path");
15091
15117
  const ffmpeg = params.ffmpegPath ?? "ffmpeg";
15092
15118
  const tmpDir = os.tmpdir();
15093
- const id = randomUUID2();
15119
+ const id = randomUUID3();
15094
15120
  const videoFormat = params.videoCodec === "H265" ? "hevc" : "h264";
15095
15121
  const videoPath = path.join(tmpDir, `reolink-${id}.${videoFormat}`);
15096
15122
  const outputPath = path.join(tmpDir, `reolink-${id}.mp4`);
@@ -20069,8 +20095,13 @@ ${scheduleItems}
20069
20095
  };
20070
20096
 
20071
20097
  // src/reolink/discovery.ts
20098
+ import { execFile } from "child_process";
20099
+ import { randomUUID as randomUUID2 } from "crypto";
20072
20100
  import dgram3 from "dgram";
20073
- import { networkInterfaces as networkInterfaces2 } from "os";
20101
+ import * as net3 from "net";
20102
+ import { networkInterfaces as networkInterfaces2, platform } from "os";
20103
+ import { promisify } from "util";
20104
+ var execFileAsync = promisify(execFile);
20074
20105
  async function discoverViaUdpDirect(host, options) {
20075
20106
  if (!options.enableUdpDiscovery) return [];
20076
20107
  const logger = options.logger;
@@ -20432,6 +20463,348 @@ async function discoverViaUdpBroadcast(options) {
20432
20463
  });
20433
20464
  });
20434
20465
  }
20466
+ var REOLINK_MAC_PREFIXES = [
20467
+ "EC:71:DB",
20468
+ // Most common Reolink OUI
20469
+ "2C:1B:3A",
20470
+ // WiFi cameras (E1 Zoom, etc.)
20471
+ "18:2C:65",
20472
+ // Battery cameras (Video Doorbell, Argus, etc.)
20473
+ "DC:E5:37",
20474
+ // Some newer models
20475
+ "9C:8E:CD",
20476
+ // Some WiFi models
20477
+ "B4:4B:D6",
20478
+ // Some models
20479
+ "E4:3D:1A"
20480
+ // Some models
20481
+ ];
20482
+ async function discoverViaArpTable(options) {
20483
+ if (!options.enableArpLookup) return [];
20484
+ const logger = options.logger;
20485
+ logger?.log?.("[Discovery] Starting ARP table lookup for Reolink MAC prefix...");
20486
+ const discovered = [];
20487
+ try {
20488
+ let entries = [];
20489
+ if (platform() === "linux") {
20490
+ try {
20491
+ const { readFile } = await import("fs/promises");
20492
+ const content = await readFile("/proc/net/arp", "utf8");
20493
+ for (const line of content.split("\n").slice(1)) {
20494
+ const parts = line.trim().split(/\s+/);
20495
+ if (parts.length >= 4 && parts[0] && parts[3] && parts[3] !== "00:00:00:00:00:00") {
20496
+ entries.push({ ip: parts[0], mac: parts[3].toUpperCase() });
20497
+ }
20498
+ }
20499
+ } catch {
20500
+ const { stdout } = await runArpCommand();
20501
+ entries = parseArpOutput(stdout);
20502
+ }
20503
+ } else {
20504
+ const { stdout } = await runArpCommand();
20505
+ entries = parseArpOutput(stdout);
20506
+ }
20507
+ logger?.log?.(`[Discovery] ARP table has ${entries.length} entries`);
20508
+ for (const { ip, mac } of entries) {
20509
+ const isReolink = REOLINK_MAC_PREFIXES.some(
20510
+ (prefix) => mac.startsWith(prefix)
20511
+ );
20512
+ if (isReolink) {
20513
+ logger?.log?.(`[Discovery] Found Reolink device via ARP: ${ip} (MAC: ${mac})`);
20514
+ discovered.push({
20515
+ host: ip,
20516
+ discoveryMethod: "arp"
20517
+ });
20518
+ }
20519
+ }
20520
+ } catch (err) {
20521
+ const msg = err instanceof Error ? err.message : String(err);
20522
+ logger?.warn?.(`[Discovery] ARP table lookup failed: ${msg}`);
20523
+ }
20524
+ logger?.log?.(`[Discovery] ARP lookup complete. Found ${discovered.length} device(s).`);
20525
+ return discovered;
20526
+ }
20527
+ async function runArpCommand() {
20528
+ const paths = ["/usr/sbin/arp", "/sbin/arp", "/usr/bin/arp", "arp"];
20529
+ for (const arpPath of paths) {
20530
+ try {
20531
+ return await execFileAsync(arpPath, ["-an"], { timeout: 5e3 });
20532
+ } catch {
20533
+ }
20534
+ }
20535
+ throw new Error("arp command not found");
20536
+ }
20537
+ function parseArpOutput(stdout) {
20538
+ const results = [];
20539
+ for (const line of stdout.split("\n")) {
20540
+ const match = /\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)/i.exec(line);
20541
+ if (match && match[1] && match[2] && match[2] !== "(incomplete)") {
20542
+ results.push({ ip: match[1], mac: match[2].toUpperCase() });
20543
+ }
20544
+ }
20545
+ return results;
20546
+ }
20547
+ async function discoverViaDhcpListener(options) {
20548
+ if (!options.enableDhcpListener) return [];
20549
+ const logger = options.logger;
20550
+ const timeoutMs = options.dhcpListenerTimeoutMs ?? 1e4;
20551
+ logger?.log?.(`[Discovery] Starting passive DHCP listener (${timeoutMs}ms)...`);
20552
+ const discovered = /* @__PURE__ */ new Map();
20553
+ return new Promise((resolve) => {
20554
+ let socket;
20555
+ let timeout;
20556
+ try {
20557
+ socket = dgram3.createSocket({ type: "udp4", reuseAddr: true });
20558
+ } catch (err) {
20559
+ logger?.warn?.(`[Discovery] DHCP: failed to create socket: ${err instanceof Error ? err.message : String(err)}`);
20560
+ resolve([]);
20561
+ return;
20562
+ }
20563
+ socket.on("message", (msg) => {
20564
+ try {
20565
+ if (msg.length < 240) return;
20566
+ const op = msg[0];
20567
+ const hlen = msg[2];
20568
+ if (hlen !== 6) return;
20569
+ const mac = [
20570
+ msg[28]?.toString(16).padStart(2, "0"),
20571
+ msg[29]?.toString(16).padStart(2, "0"),
20572
+ msg[30]?.toString(16).padStart(2, "0"),
20573
+ msg[31]?.toString(16).padStart(2, "0"),
20574
+ msg[32]?.toString(16).padStart(2, "0"),
20575
+ msg[33]?.toString(16).padStart(2, "0")
20576
+ ].join(":").toUpperCase();
20577
+ const isReolinkMac = REOLINK_MAC_PREFIXES.some((p) => mac.startsWith(p));
20578
+ let hostname = "";
20579
+ let i = 240;
20580
+ while (i < msg.length - 1) {
20581
+ const optType = msg[i];
20582
+ if (optType === 255) break;
20583
+ if (optType === 0) {
20584
+ i++;
20585
+ continue;
20586
+ }
20587
+ const optLen = msg[i + 1] ?? 0;
20588
+ if (optType === 12 && optLen > 0) {
20589
+ hostname = msg.subarray(i + 2, i + 2 + optLen).toString("ascii").toLowerCase();
20590
+ }
20591
+ i += 2 + optLen;
20592
+ }
20593
+ const isReolinkHostname = hostname.startsWith("reolink");
20594
+ if (!isReolinkMac && !isReolinkHostname) return;
20595
+ const yiaddr = `${msg[16]}.${msg[17]}.${msg[18]}.${msg[19]}`;
20596
+ const ciaddr = `${msg[12]}.${msg[13]}.${msg[14]}.${msg[15]}`;
20597
+ const ip = yiaddr !== "0.0.0.0" ? yiaddr : ciaddr;
20598
+ if (ip === "0.0.0.0" || !ip) return;
20599
+ if (!discovered.has(ip)) {
20600
+ logger?.log?.(`[Discovery] DHCP: found Reolink device ${ip} (MAC: ${mac}, hostname: ${hostname || "n/a"}, op: ${op === 1 ? "request" : "reply"})`);
20601
+ const device = {
20602
+ host: ip,
20603
+ discoveryMethod: "dhcp"
20604
+ };
20605
+ if (hostname) device.name = hostname;
20606
+ discovered.set(ip, device);
20607
+ }
20608
+ } catch {
20609
+ }
20610
+ });
20611
+ socket.on("error", (err) => {
20612
+ logger?.warn?.(`[Discovery] DHCP socket error: ${err.message}`);
20613
+ clearTimeout(timeout);
20614
+ socket.close();
20615
+ resolve(Array.from(discovered.values()));
20616
+ });
20617
+ socket.bind(67, "0.0.0.0", () => {
20618
+ logger?.log?.("[Discovery] DHCP listener bound on port 67");
20619
+ timeout = setTimeout(() => {
20620
+ socket.close();
20621
+ logger?.log?.(`[Discovery] DHCP listener complete. Found ${discovered.size} device(s).`);
20622
+ resolve(Array.from(discovered.values()));
20623
+ }, timeoutMs);
20624
+ });
20625
+ });
20626
+ }
20627
+ function probeTcpPort(ip, port, timeoutMs) {
20628
+ return new Promise((resolve) => {
20629
+ const socket = new net3.Socket();
20630
+ let settled = false;
20631
+ const done = (result) => {
20632
+ if (settled) return;
20633
+ settled = true;
20634
+ socket.destroy();
20635
+ resolve(result);
20636
+ };
20637
+ socket.setTimeout(timeoutMs);
20638
+ socket.on("connect", () => done(true));
20639
+ socket.on("timeout", () => done(false));
20640
+ socket.on("error", () => done(false));
20641
+ socket.connect(port, ip);
20642
+ });
20643
+ }
20644
+ async function discoverViaTcpPortScan(options) {
20645
+ if (!options.enableTcpPortScan) return [];
20646
+ const logger = options.logger;
20647
+ const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
20648
+ const timeoutMs = options.tcpProbeTimeoutMs ?? 1500;
20649
+ const maxConcurrent = options.maxConcurrentProbes ?? 80;
20650
+ if (!networkCidr) {
20651
+ logger?.warn?.("[Discovery] No network CIDR available for TCP port scan");
20652
+ return [];
20653
+ }
20654
+ logger?.log?.(`[Discovery] Starting TCP port 9000 scan on network ${networkCidr}...`);
20655
+ const ipRange = parseCidr(networkCidr);
20656
+ if (!ipRange) {
20657
+ logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
20658
+ return [];
20659
+ }
20660
+ const discovered = [];
20661
+ const ipAddresses = [];
20662
+ for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) {
20663
+ ipAddresses.push(ipNumberToString(ipNum));
20664
+ }
20665
+ logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IPs on port 9000...`);
20666
+ for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
20667
+ const batch = ipAddresses.slice(i, i + maxConcurrent);
20668
+ const batchResults = await Promise.allSettled(
20669
+ batch.map(async (ip) => {
20670
+ const open = await probeTcpPort(ip, 9e3, timeoutMs);
20671
+ if (open) {
20672
+ logger?.log?.(`[Discovery] Found Baichuan device at ${ip}:9000`);
20673
+ return { host: ip, discoveryMethod: "tcp_port_scan" };
20674
+ }
20675
+ return null;
20676
+ })
20677
+ );
20678
+ for (const result of batchResults) {
20679
+ if (result.status === "fulfilled" && result.value) {
20680
+ discovered.push(result.value);
20681
+ }
20682
+ }
20683
+ }
20684
+ logger?.log?.(`[Discovery] TCP port scan complete. Found ${discovered.length} device(s).`);
20685
+ return discovered;
20686
+ }
20687
+ async function discoverViaOnvif(options) {
20688
+ if (!options.enableOnvifDiscovery) return [];
20689
+ const logger = options.logger;
20690
+ const timeoutMs = options.onvifDiscoveryTimeoutMs ?? 5e3;
20691
+ logger?.log?.(`[Discovery] Starting ONVIF WS-Discovery (${timeoutMs}ms)...`);
20692
+ const discovered = /* @__PURE__ */ new Map();
20693
+ const MULTICAST_ADDR = "239.255.255.250";
20694
+ const MULTICAST_PORT = 3702;
20695
+ const messageId = `uuid:${randomUUID2()}`;
20696
+ const probeMessage = [
20697
+ '<?xml version="1.0" encoding="UTF-8"?>',
20698
+ '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"',
20699
+ ' xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"',
20700
+ ' xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"',
20701
+ ' xmlns:dn="http://www.onvif.org/ver10/network/wsdl">',
20702
+ " <s:Header>",
20703
+ ` <a:MessageID>${messageId}</a:MessageID>`,
20704
+ " <a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>",
20705
+ " <a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>",
20706
+ " </s:Header>",
20707
+ " <s:Body>",
20708
+ " <d:Probe>",
20709
+ " <d:Types>dn:NetworkVideoTransmitter</d:Types>",
20710
+ " </d:Probe>",
20711
+ " </s:Body>",
20712
+ "</s:Envelope>"
20713
+ ].join("\n");
20714
+ return new Promise((resolve) => {
20715
+ const socket = dgram3.createSocket({ type: "udp4", reuseAddr: true });
20716
+ let timeout;
20717
+ socket.on("message", (msg, rinfo) => {
20718
+ try {
20719
+ const xml = msg.toString("utf8");
20720
+ const xaddrsMatch = /<[^:]*:?XAddrs>([^<]+)<\/[^:]*:?XAddrs>/i.exec(xml);
20721
+ const scopesMatch = /<[^:]*:?Scopes>([^<]+)<\/[^:]*:?Scopes>/i.exec(xml);
20722
+ let host = rinfo.address;
20723
+ let httpPort;
20724
+ if (xaddrsMatch?.[1]) {
20725
+ const urls = xaddrsMatch[1].trim().split(/\s+/);
20726
+ for (const url of urls) {
20727
+ try {
20728
+ const parsed = new URL(url);
20729
+ if (parsed.hostname) {
20730
+ host = parsed.hostname;
20731
+ const p = Number.parseInt(parsed.port, 10);
20732
+ if (p && p !== 80) httpPort = p;
20733
+ break;
20734
+ }
20735
+ } catch {
20736
+ }
20737
+ }
20738
+ }
20739
+ if (discovered.has(host)) return;
20740
+ let model;
20741
+ let name;
20742
+ let manufacturer;
20743
+ if (scopesMatch?.[1]) {
20744
+ const scopes = scopesMatch[1].trim().split(/\s+/);
20745
+ for (const scope of scopes) {
20746
+ const hwMatch = /\/hardware\/(.+)$/i.exec(scope);
20747
+ if (hwMatch?.[1]) model = decodeURIComponent(hwMatch[1]);
20748
+ const nameMatch = /\/name\/(.+)$/i.exec(scope);
20749
+ if (nameMatch?.[1]) name = decodeURIComponent(nameMatch[1]);
20750
+ const mfgMatch = /\/manufacturer\/(.+)$/i.exec(scope);
20751
+ if (mfgMatch?.[1]) manufacturer = decodeURIComponent(mfgMatch[1]);
20752
+ }
20753
+ }
20754
+ const allText = `${manufacturer ?? ""} ${model ?? ""} ${xaddrsMatch?.[1] ?? ""}`.toLowerCase();
20755
+ const hasReolinkText = allText.includes("reolink");
20756
+ const hasReolinkModel = /^(rlc|rln|rl[ncb]|e1|cw|cx|duo|trackmix|argus|lumus|go|video doorbell|reolink)/i.test(model ?? "");
20757
+ const isReolink = hasReolinkText || hasReolinkModel;
20758
+ if (!isReolink) {
20759
+ logger?.debug?.(`[Discovery] ONVIF: skipping non-Reolink device at ${host} (${model ?? "unknown"}, manufacturer: ${manufacturer ?? "unknown"})`);
20760
+ return;
20761
+ }
20762
+ logger?.log?.(`[Discovery] ONVIF: found Reolink device at ${host}${model ? ` (${model})` : ""}${name ? ` name="${name}"` : ""}`);
20763
+ const device = {
20764
+ host,
20765
+ discoveryMethod: "onvif"
20766
+ };
20767
+ if (model) device.model = model;
20768
+ if (name && name !== "IPC") {
20769
+ device.name = name;
20770
+ } else if (model) {
20771
+ device.name = model;
20772
+ }
20773
+ if (httpPort) device.httpPort = httpPort;
20774
+ discovered.set(host, device);
20775
+ } catch {
20776
+ }
20777
+ });
20778
+ socket.on("error", (err) => {
20779
+ logger?.warn?.(`[Discovery] ONVIF socket error: ${err.message}`);
20780
+ });
20781
+ socket.bind(0, "0.0.0.0", () => {
20782
+ const buf = Buffer.from(probeMessage, "utf8");
20783
+ socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
20784
+ if (err) {
20785
+ logger?.warn?.(`[Discovery] ONVIF: failed to send probe: ${err.message}`);
20786
+ }
20787
+ });
20788
+ setTimeout(() => {
20789
+ try {
20790
+ socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR);
20791
+ } catch {
20792
+ }
20793
+ }, 500);
20794
+ timeout = setTimeout(() => {
20795
+ try {
20796
+ socket.close();
20797
+ } catch {
20798
+ }
20799
+ logger?.log?.(`[Discovery] ONVIF WS-Discovery complete. Found ${discovered.size} device(s).`);
20800
+ resolve(Array.from(discovered.values()));
20801
+ }, timeoutMs);
20802
+ });
20803
+ socket.on("close", () => {
20804
+ if (timeout) clearTimeout(timeout);
20805
+ });
20806
+ });
20807
+ }
20435
20808
  async function discoverReolinkDevices(options = {}) {
20436
20809
  const logger = options.logger;
20437
20810
  logger?.log?.("[Discovery] Starting Reolink device discovery...");
@@ -20454,10 +20827,26 @@ async function discoverReolinkDevices(options = {}) {
20454
20827
  results.push(seenDevices.get(key));
20455
20828
  }
20456
20829
  };
20457
- const [httpDevices, udpDevices] = await Promise.all([
20830
+ const [httpDevices, udpDevices, tcpDevices, arpDevices, dhcpDevices, onvifDevices] = await Promise.all([
20458
20831
  discoverViaHttpScan(options),
20459
- discoverViaUdpBroadcast(options)
20832
+ discoverViaUdpBroadcast(options),
20833
+ discoverViaTcpPortScan(options),
20834
+ discoverViaArpTable(options),
20835
+ discoverViaDhcpListener(options),
20836
+ discoverViaOnvif(options)
20460
20837
  ]);
20838
+ for (const device of dhcpDevices) {
20839
+ mergeDevice(device);
20840
+ }
20841
+ for (const device of arpDevices) {
20842
+ mergeDevice(device);
20843
+ }
20844
+ for (const device of tcpDevices) {
20845
+ mergeDevice(device);
20846
+ }
20847
+ for (const device of onvifDevices) {
20848
+ mergeDevice(device);
20849
+ }
20461
20850
  for (const device of httpDevices) {
20462
20851
  mergeDevice(device);
20463
20852
  }
@@ -20534,8 +20923,8 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
20534
20923
  async function pingHost(host, timeoutMs = 3e3) {
20535
20924
  return new Promise((resolve) => {
20536
20925
  const { exec } = __require("child_process");
20537
- const platform = process.platform;
20538
- const pingCmd = platform === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform === "darwin" ? (
20926
+ const platform2 = process.platform;
20927
+ const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
20539
20928
  // macOS: -W is in milliseconds (Linux: seconds)
20540
20929
  `ping -c 1 -W ${timeoutMs} ${host}`
20541
20930
  ) : (
@@ -21076,10 +21465,14 @@ export {
21076
21465
  discoverViaUdpDirect,
21077
21466
  discoverViaHttpScan,
21078
21467
  discoverViaUdpBroadcast,
21468
+ discoverViaArpTable,
21469
+ discoverViaDhcpListener,
21470
+ discoverViaTcpPortScan,
21471
+ discoverViaOnvif,
21079
21472
  discoverReolinkDevices,
21080
21473
  normalizeUid,
21081
21474
  maskUid,
21082
21475
  isTcpFailureThatShouldFallbackToUdp,
21083
21476
  autoDetectDeviceType
21084
21477
  };
21085
- //# sourceMappingURL=chunk-EG5IY3CM.js.map
21478
+ //# sourceMappingURL=chunk-YSEFEQYV.js.map