@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 +6 -2
- package/dist/{chunk-EG5IY3CM.js → chunk-YSEFEQYV.js} +412 -19
- package/dist/chunk-YSEFEQYV.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +46 -15
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +480 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -70
- package/dist/index.d.ts +78 -69
- package/dist/index.js +46 -128
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-EG5IY3CM.js.map +0 -1
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
|
-
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
20538
|
-
const pingCmd =
|
|
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-
|
|
21478
|
+
//# sourceMappingURL=chunk-YSEFEQYV.js.map
|