@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.
- package/README.md +48 -82
- package/dist/{chunk-EG5IY3CM.js → chunk-UDS2UR4S.js} +444 -20
- package/dist/chunk-UDS2UR4S.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +78 -16
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +1071 -210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +211 -70
- package/dist/index.d.ts +193 -69
- package/dist/index.js +580 -142
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/chunk-EG5IY3CM.js.map +0 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
20538
|
-
const pingCmd =
|
|
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-
|
|
21509
|
+
//# sourceMappingURL=chunk-UDS2UR4S.js.map
|