@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
package/dist/index.cjs
CHANGED
|
@@ -7444,6 +7444,7 @@ __export(index_exports, {
|
|
|
7444
7444
|
DUAL_LENS_DUAL_MOTION_MODELS: () => DUAL_LENS_DUAL_MOTION_MODELS,
|
|
7445
7445
|
DUAL_LENS_MODELS: () => DUAL_LENS_MODELS,
|
|
7446
7446
|
DUAL_LENS_SINGLE_MOTION_MODELS: () => DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
7447
|
+
Go2rtcTcpServer: () => Go2rtcTcpServer,
|
|
7447
7448
|
H264RtpDepacketizer: () => H264RtpDepacketizer,
|
|
7448
7449
|
H265RtpDepacketizer: () => H265RtpDepacketizer,
|
|
7449
7450
|
HlsSessionManager: () => HlsSessionManager,
|
|
@@ -7511,7 +7512,11 @@ __export(index_exports, {
|
|
|
7511
7512
|
detectIosClient: () => detectIosClient,
|
|
7512
7513
|
detectVideoCodecFromNal: () => detectVideoCodecFromNal,
|
|
7513
7514
|
discoverReolinkDevices: () => discoverReolinkDevices,
|
|
7515
|
+
discoverViaArpTable: () => discoverViaArpTable,
|
|
7516
|
+
discoverViaDhcpListener: () => discoverViaDhcpListener,
|
|
7514
7517
|
discoverViaHttpScan: () => discoverViaHttpScan,
|
|
7518
|
+
discoverViaOnvif: () => discoverViaOnvif,
|
|
7519
|
+
discoverViaTcpPortScan: () => discoverViaTcpPortScan,
|
|
7515
7520
|
discoverViaUdpBroadcast: () => discoverViaUdpBroadcast,
|
|
7516
7521
|
discoverViaUdpDirect: () => discoverViaUdpDirect,
|
|
7517
7522
|
encodeHeader: () => encodeHeader,
|
|
@@ -9257,6 +9262,22 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
9257
9262
|
static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
|
|
9258
9263
|
static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
|
|
9259
9264
|
static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
|
|
9265
|
+
/**
|
|
9266
|
+
* Per-client snapshot (cmd_id=109) serialization queue.
|
|
9267
|
+
*
|
|
9268
|
+
* WHY: On NVR/multi-camera devices sharing one socket, concurrent snapshot requests
|
|
9269
|
+
* can cause JPEG data to mix (even with per-request msgNum filtering):
|
|
9270
|
+
* - Camera A and B both send frames on same socket
|
|
9271
|
+
* - Frame listener is global per socket
|
|
9272
|
+
* - Timing quirks can cause chunk reordering or listener confusion
|
|
9273
|
+
*
|
|
9274
|
+
* FIX: Serialize all cmd_id=109 requests on THIS client instance.
|
|
9275
|
+
* Each snapshot waits for previous one to complete before starting.
|
|
9276
|
+
* This ensures clean frame sequences per request, zero data corruption.
|
|
9277
|
+
*
|
|
9278
|
+
* Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
|
|
9279
|
+
*/
|
|
9280
|
+
snapshotQueueTail = Promise.resolve();
|
|
9260
9281
|
opts;
|
|
9261
9282
|
debugCfg;
|
|
9262
9283
|
logger;
|
|
@@ -11738,6 +11759,20 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11738
11759
|
});
|
|
11739
11760
|
}
|
|
11740
11761
|
async sendBinarySnapshot109(params) {
|
|
11762
|
+
const prevTail = this.snapshotQueueTail;
|
|
11763
|
+
let resolve;
|
|
11764
|
+
const newTail = new Promise((r) => {
|
|
11765
|
+
resolve = r;
|
|
11766
|
+
});
|
|
11767
|
+
this.snapshotQueueTail = newTail;
|
|
11768
|
+
try {
|
|
11769
|
+
await prevTail;
|
|
11770
|
+
return await this.sendBinarySnapshot109Impl(params);
|
|
11771
|
+
} finally {
|
|
11772
|
+
resolve();
|
|
11773
|
+
}
|
|
11774
|
+
}
|
|
11775
|
+
async sendBinarySnapshot109Impl(params) {
|
|
11741
11776
|
await this.connect();
|
|
11742
11777
|
const channel = params.channel ?? this.opts.channel ?? 0;
|
|
11743
11778
|
const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
|
|
@@ -11797,7 +11832,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11797
11832
|
};
|
|
11798
11833
|
const onFrame = (frame) => {
|
|
11799
11834
|
if (frame.header.cmdId !== cmdId) return;
|
|
11800
|
-
if (frame.header.msgNum
|
|
11835
|
+
if (frame.header.msgNum !== msgNum) return;
|
|
11836
|
+
if (frame.header.responseCode >= 400) {
|
|
11801
11837
|
fail(
|
|
11802
11838
|
new Error(
|
|
11803
11839
|
`Baichuan snapshot request rejected (cmdId=${cmdId} msgNum=${msgNum} responseCode=${frame.header.responseCode})`
|
|
@@ -13379,14 +13415,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13379
13415
|
`;
|
|
13380
13416
|
}
|
|
13381
13417
|
if (body) {
|
|
13382
|
-
|
|
13418
|
+
const bodyBuf = Buffer.from(body, "utf8");
|
|
13419
|
+
response += `Content-Length: ${bodyBuf.length}\r
|
|
13383
13420
|
`;
|
|
13421
|
+
response += "\r\n";
|
|
13422
|
+
socket.write(response);
|
|
13423
|
+
socket.write(bodyBuf);
|
|
13424
|
+
} else {
|
|
13425
|
+
response += "\r\n";
|
|
13426
|
+
socket.write(response);
|
|
13384
13427
|
}
|
|
13385
|
-
response += "\r\n";
|
|
13386
|
-
if (body) {
|
|
13387
|
-
response += body;
|
|
13388
|
-
}
|
|
13389
|
-
socket.write(response);
|
|
13390
13428
|
};
|
|
13391
13429
|
this.rtspDebugLog(`RTSP ${method} ${url}`);
|
|
13392
13430
|
if (this.requireAuth) {
|
|
@@ -13596,10 +13634,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13596
13634
|
);
|
|
13597
13635
|
}
|
|
13598
13636
|
}
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13637
|
+
{
|
|
13638
|
+
const baseUrl = `rtsp://${this.listenHost}:${this.listenPort}${this.path}`;
|
|
13639
|
+
const resources = this.clientResources.get(clientId);
|
|
13640
|
+
const rtpInfoParts = [];
|
|
13641
|
+
if (resources?.setupTrack0) {
|
|
13642
|
+
rtpInfoParts.push(`url=${baseUrl}/track0`);
|
|
13643
|
+
}
|
|
13644
|
+
if (resources?.setupTrack1) {
|
|
13645
|
+
rtpInfoParts.push(`url=${baseUrl}/track1`);
|
|
13646
|
+
}
|
|
13647
|
+
const playHeaders = {
|
|
13648
|
+
Session: sessionId,
|
|
13649
|
+
Range: "npt=now-"
|
|
13650
|
+
};
|
|
13651
|
+
if (rtpInfoParts.length > 0) {
|
|
13652
|
+
playHeaders["RTP-Info"] = rtpInfoParts.join(",");
|
|
13653
|
+
}
|
|
13654
|
+
sendResponse(200, "OK", playHeaders);
|
|
13655
|
+
}
|
|
13603
13656
|
} else if (method === "TEARDOWN") {
|
|
13604
13657
|
this.logger.info(
|
|
13605
13658
|
`[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
|
|
@@ -13629,6 +13682,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
13629
13682
|
sdp += `c=IN IP4 ${this.listenHost}\r
|
|
13630
13683
|
`;
|
|
13631
13684
|
sdp += "t=0 0\r\n";
|
|
13685
|
+
sdp += "a=range:npt=now-\r\n";
|
|
13686
|
+
sdp += "a=control:*\r\n";
|
|
13632
13687
|
sdp += `m=video 0 RTP/AVP ${videoPayloadType}\r
|
|
13633
13688
|
`;
|
|
13634
13689
|
sdp += `a=rtpmap:${videoPayloadType} ${codec}/90000\r
|
|
@@ -14571,7 +14626,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
|
|
|
14571
14626
|
this.firstFramePromise = null;
|
|
14572
14627
|
this.firstFrameResolve = null;
|
|
14573
14628
|
this.nativeFanout = null;
|
|
14574
|
-
|
|
14629
|
+
for (const [, resources] of this.clientResources) {
|
|
14630
|
+
const res = resources;
|
|
14631
|
+
res.rtpVideoBaseMicroseconds = void 0;
|
|
14632
|
+
res.rtpVideoBaseTimestamp = void 0;
|
|
14633
|
+
res.rtpVideoLastTimestamp = void 0;
|
|
14634
|
+
res.seenFirstVideoKeyframe = false;
|
|
14635
|
+
res.rtpSentVideoConfig = false;
|
|
14636
|
+
}
|
|
14575
14637
|
if (this.dedicatedSessionRelease) {
|
|
14576
14638
|
const release = this.dedicatedSessionRelease;
|
|
14577
14639
|
this.dedicatedSessionRelease = void 0;
|
|
@@ -22571,13 +22633,13 @@ ${stderr}`)
|
|
|
22571
22633
|
*/
|
|
22572
22634
|
async muxToMp4(params) {
|
|
22573
22635
|
const { spawn: spawn12 } = await import("child_process");
|
|
22574
|
-
const { randomUUID:
|
|
22636
|
+
const { randomUUID: randomUUID3 } = await import("crypto");
|
|
22575
22637
|
const fs6 = await import("fs/promises");
|
|
22576
22638
|
const os2 = await import("os");
|
|
22577
22639
|
const path6 = await import("path");
|
|
22578
22640
|
const ffmpeg = params.ffmpegPath ?? "ffmpeg";
|
|
22579
22641
|
const tmpDir = os2.tmpdir();
|
|
22580
|
-
const id =
|
|
22642
|
+
const id = randomUUID3();
|
|
22581
22643
|
const videoFormat = params.videoCodec === "H265" ? "hevc" : "h264";
|
|
22582
22644
|
const videoPath = path6.join(tmpDir, `reolink-${id}.${videoFormat}`);
|
|
22583
22645
|
const outputPath = path6.join(tmpDir, `reolink-${id}.mp4`);
|
|
@@ -27918,9 +27980,14 @@ function buildHlsRedirectUrl(originalUrl) {
|
|
|
27918
27980
|
}
|
|
27919
27981
|
|
|
27920
27982
|
// src/reolink/discovery.ts
|
|
27983
|
+
var import_node_child_process4 = require("child_process");
|
|
27984
|
+
var import_node_crypto3 = require("crypto");
|
|
27921
27985
|
var import_node_dgram2 = __toESM(require("dgram"), 1);
|
|
27986
|
+
var net3 = __toESM(require("net"), 1);
|
|
27922
27987
|
var import_node_os2 = require("os");
|
|
27988
|
+
var import_node_util = require("util");
|
|
27923
27989
|
init_ReolinkCgiApi();
|
|
27990
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process4.execFile);
|
|
27924
27991
|
async function discoverViaUdpDirect(host, options) {
|
|
27925
27992
|
if (!options.enableUdpDiscovery) return [];
|
|
27926
27993
|
const logger = options.logger;
|
|
@@ -28282,6 +28349,348 @@ async function discoverViaUdpBroadcast(options) {
|
|
|
28282
28349
|
});
|
|
28283
28350
|
});
|
|
28284
28351
|
}
|
|
28352
|
+
var REOLINK_MAC_PREFIXES = [
|
|
28353
|
+
"EC:71:DB",
|
|
28354
|
+
// Most common Reolink OUI
|
|
28355
|
+
"2C:1B:3A",
|
|
28356
|
+
// WiFi cameras (E1 Zoom, etc.)
|
|
28357
|
+
"18:2C:65",
|
|
28358
|
+
// Battery cameras (Video Doorbell, Argus, etc.)
|
|
28359
|
+
"DC:E5:37",
|
|
28360
|
+
// Some newer models
|
|
28361
|
+
"9C:8E:CD",
|
|
28362
|
+
// Some WiFi models
|
|
28363
|
+
"B4:4B:D6",
|
|
28364
|
+
// Some models
|
|
28365
|
+
"E4:3D:1A"
|
|
28366
|
+
// Some models
|
|
28367
|
+
];
|
|
28368
|
+
async function discoverViaArpTable(options) {
|
|
28369
|
+
if (!options.enableArpLookup) return [];
|
|
28370
|
+
const logger = options.logger;
|
|
28371
|
+
logger?.log?.("[Discovery] Starting ARP table lookup for Reolink MAC prefix...");
|
|
28372
|
+
const discovered = [];
|
|
28373
|
+
try {
|
|
28374
|
+
let entries = [];
|
|
28375
|
+
if ((0, import_node_os2.platform)() === "linux") {
|
|
28376
|
+
try {
|
|
28377
|
+
const { readFile } = await import("fs/promises");
|
|
28378
|
+
const content = await readFile("/proc/net/arp", "utf8");
|
|
28379
|
+
for (const line of content.split("\n").slice(1)) {
|
|
28380
|
+
const parts = line.trim().split(/\s+/);
|
|
28381
|
+
if (parts.length >= 4 && parts[0] && parts[3] && parts[3] !== "00:00:00:00:00:00") {
|
|
28382
|
+
entries.push({ ip: parts[0], mac: parts[3].toUpperCase() });
|
|
28383
|
+
}
|
|
28384
|
+
}
|
|
28385
|
+
} catch {
|
|
28386
|
+
const { stdout } = await runArpCommand();
|
|
28387
|
+
entries = parseArpOutput(stdout);
|
|
28388
|
+
}
|
|
28389
|
+
} else {
|
|
28390
|
+
const { stdout } = await runArpCommand();
|
|
28391
|
+
entries = parseArpOutput(stdout);
|
|
28392
|
+
}
|
|
28393
|
+
logger?.log?.(`[Discovery] ARP table has ${entries.length} entries`);
|
|
28394
|
+
for (const { ip, mac } of entries) {
|
|
28395
|
+
const isReolink = REOLINK_MAC_PREFIXES.some(
|
|
28396
|
+
(prefix) => mac.startsWith(prefix)
|
|
28397
|
+
);
|
|
28398
|
+
if (isReolink) {
|
|
28399
|
+
logger?.log?.(`[Discovery] Found Reolink device via ARP: ${ip} (MAC: ${mac})`);
|
|
28400
|
+
discovered.push({
|
|
28401
|
+
host: ip,
|
|
28402
|
+
discoveryMethod: "arp"
|
|
28403
|
+
});
|
|
28404
|
+
}
|
|
28405
|
+
}
|
|
28406
|
+
} catch (err) {
|
|
28407
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28408
|
+
logger?.warn?.(`[Discovery] ARP table lookup failed: ${msg}`);
|
|
28409
|
+
}
|
|
28410
|
+
logger?.log?.(`[Discovery] ARP lookup complete. Found ${discovered.length} device(s).`);
|
|
28411
|
+
return discovered;
|
|
28412
|
+
}
|
|
28413
|
+
async function runArpCommand() {
|
|
28414
|
+
const paths = ["/usr/sbin/arp", "/sbin/arp", "/usr/bin/arp", "arp"];
|
|
28415
|
+
for (const arpPath of paths) {
|
|
28416
|
+
try {
|
|
28417
|
+
return await execFileAsync(arpPath, ["-an"], { timeout: 5e3 });
|
|
28418
|
+
} catch {
|
|
28419
|
+
}
|
|
28420
|
+
}
|
|
28421
|
+
throw new Error("arp command not found");
|
|
28422
|
+
}
|
|
28423
|
+
function parseArpOutput(stdout) {
|
|
28424
|
+
const results = [];
|
|
28425
|
+
for (const line of stdout.split("\n")) {
|
|
28426
|
+
const match = /\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)/i.exec(line);
|
|
28427
|
+
if (match && match[1] && match[2] && match[2] !== "(incomplete)") {
|
|
28428
|
+
results.push({ ip: match[1], mac: match[2].toUpperCase() });
|
|
28429
|
+
}
|
|
28430
|
+
}
|
|
28431
|
+
return results;
|
|
28432
|
+
}
|
|
28433
|
+
async function discoverViaDhcpListener(options) {
|
|
28434
|
+
if (!options.enableDhcpListener) return [];
|
|
28435
|
+
const logger = options.logger;
|
|
28436
|
+
const timeoutMs = options.dhcpListenerTimeoutMs ?? 1e4;
|
|
28437
|
+
logger?.log?.(`[Discovery] Starting passive DHCP listener (${timeoutMs}ms)...`);
|
|
28438
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
28439
|
+
return new Promise((resolve) => {
|
|
28440
|
+
let socket;
|
|
28441
|
+
let timeout;
|
|
28442
|
+
try {
|
|
28443
|
+
socket = import_node_dgram2.default.createSocket({ type: "udp4", reuseAddr: true });
|
|
28444
|
+
} catch (err) {
|
|
28445
|
+
logger?.warn?.(`[Discovery] DHCP: failed to create socket: ${err instanceof Error ? err.message : String(err)}`);
|
|
28446
|
+
resolve([]);
|
|
28447
|
+
return;
|
|
28448
|
+
}
|
|
28449
|
+
socket.on("message", (msg) => {
|
|
28450
|
+
try {
|
|
28451
|
+
if (msg.length < 240) return;
|
|
28452
|
+
const op = msg[0];
|
|
28453
|
+
const hlen = msg[2];
|
|
28454
|
+
if (hlen !== 6) return;
|
|
28455
|
+
const mac = [
|
|
28456
|
+
msg[28]?.toString(16).padStart(2, "0"),
|
|
28457
|
+
msg[29]?.toString(16).padStart(2, "0"),
|
|
28458
|
+
msg[30]?.toString(16).padStart(2, "0"),
|
|
28459
|
+
msg[31]?.toString(16).padStart(2, "0"),
|
|
28460
|
+
msg[32]?.toString(16).padStart(2, "0"),
|
|
28461
|
+
msg[33]?.toString(16).padStart(2, "0")
|
|
28462
|
+
].join(":").toUpperCase();
|
|
28463
|
+
const isReolinkMac = REOLINK_MAC_PREFIXES.some((p) => mac.startsWith(p));
|
|
28464
|
+
let hostname = "";
|
|
28465
|
+
let i = 240;
|
|
28466
|
+
while (i < msg.length - 1) {
|
|
28467
|
+
const optType = msg[i];
|
|
28468
|
+
if (optType === 255) break;
|
|
28469
|
+
if (optType === 0) {
|
|
28470
|
+
i++;
|
|
28471
|
+
continue;
|
|
28472
|
+
}
|
|
28473
|
+
const optLen = msg[i + 1] ?? 0;
|
|
28474
|
+
if (optType === 12 && optLen > 0) {
|
|
28475
|
+
hostname = msg.subarray(i + 2, i + 2 + optLen).toString("ascii").toLowerCase();
|
|
28476
|
+
}
|
|
28477
|
+
i += 2 + optLen;
|
|
28478
|
+
}
|
|
28479
|
+
const isReolinkHostname = hostname.startsWith("reolink");
|
|
28480
|
+
if (!isReolinkMac && !isReolinkHostname) return;
|
|
28481
|
+
const yiaddr = `${msg[16]}.${msg[17]}.${msg[18]}.${msg[19]}`;
|
|
28482
|
+
const ciaddr = `${msg[12]}.${msg[13]}.${msg[14]}.${msg[15]}`;
|
|
28483
|
+
const ip = yiaddr !== "0.0.0.0" ? yiaddr : ciaddr;
|
|
28484
|
+
if (ip === "0.0.0.0" || !ip) return;
|
|
28485
|
+
if (!discovered.has(ip)) {
|
|
28486
|
+
logger?.log?.(`[Discovery] DHCP: found Reolink device ${ip} (MAC: ${mac}, hostname: ${hostname || "n/a"}, op: ${op === 1 ? "request" : "reply"})`);
|
|
28487
|
+
const device = {
|
|
28488
|
+
host: ip,
|
|
28489
|
+
discoveryMethod: "dhcp"
|
|
28490
|
+
};
|
|
28491
|
+
if (hostname) device.name = hostname;
|
|
28492
|
+
discovered.set(ip, device);
|
|
28493
|
+
}
|
|
28494
|
+
} catch {
|
|
28495
|
+
}
|
|
28496
|
+
});
|
|
28497
|
+
socket.on("error", (err) => {
|
|
28498
|
+
logger?.warn?.(`[Discovery] DHCP socket error: ${err.message}`);
|
|
28499
|
+
clearTimeout(timeout);
|
|
28500
|
+
socket.close();
|
|
28501
|
+
resolve(Array.from(discovered.values()));
|
|
28502
|
+
});
|
|
28503
|
+
socket.bind(67, "0.0.0.0", () => {
|
|
28504
|
+
logger?.log?.("[Discovery] DHCP listener bound on port 67");
|
|
28505
|
+
timeout = setTimeout(() => {
|
|
28506
|
+
socket.close();
|
|
28507
|
+
logger?.log?.(`[Discovery] DHCP listener complete. Found ${discovered.size} device(s).`);
|
|
28508
|
+
resolve(Array.from(discovered.values()));
|
|
28509
|
+
}, timeoutMs);
|
|
28510
|
+
});
|
|
28511
|
+
});
|
|
28512
|
+
}
|
|
28513
|
+
function probeTcpPort(ip, port, timeoutMs) {
|
|
28514
|
+
return new Promise((resolve) => {
|
|
28515
|
+
const socket = new net3.Socket();
|
|
28516
|
+
let settled = false;
|
|
28517
|
+
const done = (result) => {
|
|
28518
|
+
if (settled) return;
|
|
28519
|
+
settled = true;
|
|
28520
|
+
socket.destroy();
|
|
28521
|
+
resolve(result);
|
|
28522
|
+
};
|
|
28523
|
+
socket.setTimeout(timeoutMs);
|
|
28524
|
+
socket.on("connect", () => done(true));
|
|
28525
|
+
socket.on("timeout", () => done(false));
|
|
28526
|
+
socket.on("error", () => done(false));
|
|
28527
|
+
socket.connect(port, ip);
|
|
28528
|
+
});
|
|
28529
|
+
}
|
|
28530
|
+
async function discoverViaTcpPortScan(options) {
|
|
28531
|
+
if (!options.enableTcpPortScan) return [];
|
|
28532
|
+
const logger = options.logger;
|
|
28533
|
+
const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
|
|
28534
|
+
const timeoutMs = options.tcpProbeTimeoutMs ?? 1500;
|
|
28535
|
+
const maxConcurrent = options.maxConcurrentProbes ?? 80;
|
|
28536
|
+
if (!networkCidr) {
|
|
28537
|
+
logger?.warn?.("[Discovery] No network CIDR available for TCP port scan");
|
|
28538
|
+
return [];
|
|
28539
|
+
}
|
|
28540
|
+
logger?.log?.(`[Discovery] Starting TCP port 9000 scan on network ${networkCidr}...`);
|
|
28541
|
+
const ipRange = parseCidr(networkCidr);
|
|
28542
|
+
if (!ipRange) {
|
|
28543
|
+
logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
|
|
28544
|
+
return [];
|
|
28545
|
+
}
|
|
28546
|
+
const discovered = [];
|
|
28547
|
+
const ipAddresses = [];
|
|
28548
|
+
for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) {
|
|
28549
|
+
ipAddresses.push(ipNumberToString(ipNum));
|
|
28550
|
+
}
|
|
28551
|
+
logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IPs on port 9000...`);
|
|
28552
|
+
for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
|
|
28553
|
+
const batch = ipAddresses.slice(i, i + maxConcurrent);
|
|
28554
|
+
const batchResults = await Promise.allSettled(
|
|
28555
|
+
batch.map(async (ip) => {
|
|
28556
|
+
const open = await probeTcpPort(ip, 9e3, timeoutMs);
|
|
28557
|
+
if (open) {
|
|
28558
|
+
logger?.log?.(`[Discovery] Found Baichuan device at ${ip}:9000`);
|
|
28559
|
+
return { host: ip, discoveryMethod: "tcp_port_scan" };
|
|
28560
|
+
}
|
|
28561
|
+
return null;
|
|
28562
|
+
})
|
|
28563
|
+
);
|
|
28564
|
+
for (const result of batchResults) {
|
|
28565
|
+
if (result.status === "fulfilled" && result.value) {
|
|
28566
|
+
discovered.push(result.value);
|
|
28567
|
+
}
|
|
28568
|
+
}
|
|
28569
|
+
}
|
|
28570
|
+
logger?.log?.(`[Discovery] TCP port scan complete. Found ${discovered.length} device(s).`);
|
|
28571
|
+
return discovered;
|
|
28572
|
+
}
|
|
28573
|
+
async function discoverViaOnvif(options) {
|
|
28574
|
+
if (!options.enableOnvifDiscovery) return [];
|
|
28575
|
+
const logger = options.logger;
|
|
28576
|
+
const timeoutMs = options.onvifDiscoveryTimeoutMs ?? 5e3;
|
|
28577
|
+
logger?.log?.(`[Discovery] Starting ONVIF WS-Discovery (${timeoutMs}ms)...`);
|
|
28578
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
28579
|
+
const MULTICAST_ADDR = "239.255.255.250";
|
|
28580
|
+
const MULTICAST_PORT = 3702;
|
|
28581
|
+
const messageId = `uuid:${(0, import_node_crypto3.randomUUID)()}`;
|
|
28582
|
+
const probeMessage = [
|
|
28583
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
28584
|
+
'<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"',
|
|
28585
|
+
' xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"',
|
|
28586
|
+
' xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"',
|
|
28587
|
+
' xmlns:dn="http://www.onvif.org/ver10/network/wsdl">',
|
|
28588
|
+
" <s:Header>",
|
|
28589
|
+
` <a:MessageID>${messageId}</a:MessageID>`,
|
|
28590
|
+
" <a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>",
|
|
28591
|
+
" <a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>",
|
|
28592
|
+
" </s:Header>",
|
|
28593
|
+
" <s:Body>",
|
|
28594
|
+
" <d:Probe>",
|
|
28595
|
+
" <d:Types>dn:NetworkVideoTransmitter</d:Types>",
|
|
28596
|
+
" </d:Probe>",
|
|
28597
|
+
" </s:Body>",
|
|
28598
|
+
"</s:Envelope>"
|
|
28599
|
+
].join("\n");
|
|
28600
|
+
return new Promise((resolve) => {
|
|
28601
|
+
const socket = import_node_dgram2.default.createSocket({ type: "udp4", reuseAddr: true });
|
|
28602
|
+
let timeout;
|
|
28603
|
+
socket.on("message", (msg, rinfo) => {
|
|
28604
|
+
try {
|
|
28605
|
+
const xml = msg.toString("utf8");
|
|
28606
|
+
const xaddrsMatch = /<[^:]*:?XAddrs>([^<]+)<\/[^:]*:?XAddrs>/i.exec(xml);
|
|
28607
|
+
const scopesMatch = /<[^:]*:?Scopes>([^<]+)<\/[^:]*:?Scopes>/i.exec(xml);
|
|
28608
|
+
let host = rinfo.address;
|
|
28609
|
+
let httpPort;
|
|
28610
|
+
if (xaddrsMatch?.[1]) {
|
|
28611
|
+
const urls = xaddrsMatch[1].trim().split(/\s+/);
|
|
28612
|
+
for (const url of urls) {
|
|
28613
|
+
try {
|
|
28614
|
+
const parsed = new URL(url);
|
|
28615
|
+
if (parsed.hostname) {
|
|
28616
|
+
host = parsed.hostname;
|
|
28617
|
+
const p = Number.parseInt(parsed.port, 10);
|
|
28618
|
+
if (p && p !== 80) httpPort = p;
|
|
28619
|
+
break;
|
|
28620
|
+
}
|
|
28621
|
+
} catch {
|
|
28622
|
+
}
|
|
28623
|
+
}
|
|
28624
|
+
}
|
|
28625
|
+
if (discovered.has(host)) return;
|
|
28626
|
+
let model;
|
|
28627
|
+
let name;
|
|
28628
|
+
let manufacturer;
|
|
28629
|
+
if (scopesMatch?.[1]) {
|
|
28630
|
+
const scopes = scopesMatch[1].trim().split(/\s+/);
|
|
28631
|
+
for (const scope of scopes) {
|
|
28632
|
+
const hwMatch = /\/hardware\/(.+)$/i.exec(scope);
|
|
28633
|
+
if (hwMatch?.[1]) model = decodeURIComponent(hwMatch[1]);
|
|
28634
|
+
const nameMatch = /\/name\/(.+)$/i.exec(scope);
|
|
28635
|
+
if (nameMatch?.[1]) name = decodeURIComponent(nameMatch[1]);
|
|
28636
|
+
const mfgMatch = /\/manufacturer\/(.+)$/i.exec(scope);
|
|
28637
|
+
if (mfgMatch?.[1]) manufacturer = decodeURIComponent(mfgMatch[1]);
|
|
28638
|
+
}
|
|
28639
|
+
}
|
|
28640
|
+
const allText = `${manufacturer ?? ""} ${model ?? ""} ${xaddrsMatch?.[1] ?? ""}`.toLowerCase();
|
|
28641
|
+
const hasReolinkText = allText.includes("reolink");
|
|
28642
|
+
const hasReolinkModel = /^(rlc|rln|rl[ncb]|e1|cw|cx|duo|trackmix|argus|lumus|go|video doorbell|reolink)/i.test(model ?? "");
|
|
28643
|
+
const isReolink = hasReolinkText || hasReolinkModel;
|
|
28644
|
+
if (!isReolink) {
|
|
28645
|
+
logger?.debug?.(`[Discovery] ONVIF: skipping non-Reolink device at ${host} (${model ?? "unknown"}, manufacturer: ${manufacturer ?? "unknown"})`);
|
|
28646
|
+
return;
|
|
28647
|
+
}
|
|
28648
|
+
logger?.log?.(`[Discovery] ONVIF: found Reolink device at ${host}${model ? ` (${model})` : ""}${name ? ` name="${name}"` : ""}`);
|
|
28649
|
+
const device = {
|
|
28650
|
+
host,
|
|
28651
|
+
discoveryMethod: "onvif"
|
|
28652
|
+
};
|
|
28653
|
+
if (model) device.model = model;
|
|
28654
|
+
if (name && name !== "IPC") {
|
|
28655
|
+
device.name = name;
|
|
28656
|
+
} else if (model) {
|
|
28657
|
+
device.name = model;
|
|
28658
|
+
}
|
|
28659
|
+
if (httpPort) device.httpPort = httpPort;
|
|
28660
|
+
discovered.set(host, device);
|
|
28661
|
+
} catch {
|
|
28662
|
+
}
|
|
28663
|
+
});
|
|
28664
|
+
socket.on("error", (err) => {
|
|
28665
|
+
logger?.warn?.(`[Discovery] ONVIF socket error: ${err.message}`);
|
|
28666
|
+
});
|
|
28667
|
+
socket.bind(0, "0.0.0.0", () => {
|
|
28668
|
+
const buf = Buffer.from(probeMessage, "utf8");
|
|
28669
|
+
socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
|
|
28670
|
+
if (err) {
|
|
28671
|
+
logger?.warn?.(`[Discovery] ONVIF: failed to send probe: ${err.message}`);
|
|
28672
|
+
}
|
|
28673
|
+
});
|
|
28674
|
+
setTimeout(() => {
|
|
28675
|
+
try {
|
|
28676
|
+
socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR);
|
|
28677
|
+
} catch {
|
|
28678
|
+
}
|
|
28679
|
+
}, 500);
|
|
28680
|
+
timeout = setTimeout(() => {
|
|
28681
|
+
try {
|
|
28682
|
+
socket.close();
|
|
28683
|
+
} catch {
|
|
28684
|
+
}
|
|
28685
|
+
logger?.log?.(`[Discovery] ONVIF WS-Discovery complete. Found ${discovered.size} device(s).`);
|
|
28686
|
+
resolve(Array.from(discovered.values()));
|
|
28687
|
+
}, timeoutMs);
|
|
28688
|
+
});
|
|
28689
|
+
socket.on("close", () => {
|
|
28690
|
+
if (timeout) clearTimeout(timeout);
|
|
28691
|
+
});
|
|
28692
|
+
});
|
|
28693
|
+
}
|
|
28285
28694
|
async function discoverReolinkDevices(options = {}) {
|
|
28286
28695
|
const logger = options.logger;
|
|
28287
28696
|
logger?.log?.("[Discovery] Starting Reolink device discovery...");
|
|
@@ -28304,10 +28713,26 @@ async function discoverReolinkDevices(options = {}) {
|
|
|
28304
28713
|
results.push(seenDevices.get(key));
|
|
28305
28714
|
}
|
|
28306
28715
|
};
|
|
28307
|
-
const [httpDevices, udpDevices] = await Promise.all([
|
|
28716
|
+
const [httpDevices, udpDevices, tcpDevices, arpDevices, dhcpDevices, onvifDevices] = await Promise.all([
|
|
28308
28717
|
discoverViaHttpScan(options),
|
|
28309
|
-
discoverViaUdpBroadcast(options)
|
|
28718
|
+
discoverViaUdpBroadcast(options),
|
|
28719
|
+
discoverViaTcpPortScan(options),
|
|
28720
|
+
discoverViaArpTable(options),
|
|
28721
|
+
discoverViaDhcpListener(options),
|
|
28722
|
+
discoverViaOnvif(options)
|
|
28310
28723
|
]);
|
|
28724
|
+
for (const device of dhcpDevices) {
|
|
28725
|
+
mergeDevice(device);
|
|
28726
|
+
}
|
|
28727
|
+
for (const device of arpDevices) {
|
|
28728
|
+
mergeDevice(device);
|
|
28729
|
+
}
|
|
28730
|
+
for (const device of tcpDevices) {
|
|
28731
|
+
mergeDevice(device);
|
|
28732
|
+
}
|
|
28733
|
+
for (const device of onvifDevices) {
|
|
28734
|
+
mergeDevice(device);
|
|
28735
|
+
}
|
|
28311
28736
|
for (const device of httpDevices) {
|
|
28312
28737
|
mergeDevice(device);
|
|
28313
28738
|
}
|
|
@@ -28325,51 +28750,18 @@ var AutodiscoveryClient = class {
|
|
|
28325
28750
|
scanTimer = null;
|
|
28326
28751
|
isRunning = false;
|
|
28327
28752
|
currentScanPromise = null;
|
|
28328
|
-
/**
|
|
28329
|
-
* Costruttore del client di autodiscovery.
|
|
28330
|
-
*
|
|
28331
|
-
* @param options - Opzioni di configurazione per il discovery
|
|
28332
|
-
*/
|
|
28333
28753
|
constructor(options = {}) {
|
|
28334
28754
|
this.options = {
|
|
28335
|
-
|
|
28336
|
-
|
|
28755
|
+
...options,
|
|
28756
|
+
scanIntervalMs: options.scanIntervalMs ?? 12e4,
|
|
28337
28757
|
autoStart: options.autoStart ?? false
|
|
28338
28758
|
};
|
|
28339
|
-
if (options.networkCidr !== void 0) {
|
|
28340
|
-
this.options.networkCidr = options.networkCidr;
|
|
28341
|
-
}
|
|
28342
|
-
if (options.username !== void 0) {
|
|
28343
|
-
this.options.username = options.username;
|
|
28344
|
-
}
|
|
28345
|
-
if (options.password !== void 0) {
|
|
28346
|
-
this.options.password = options.password;
|
|
28347
|
-
}
|
|
28348
|
-
if (options.httpProbeTimeoutMs !== void 0) {
|
|
28349
|
-
this.options.httpProbeTimeoutMs = options.httpProbeTimeoutMs;
|
|
28350
|
-
}
|
|
28351
|
-
if (options.maxConcurrentProbes !== void 0) {
|
|
28352
|
-
this.options.maxConcurrentProbes = options.maxConcurrentProbes;
|
|
28353
|
-
}
|
|
28354
|
-
if (options.logger !== void 0) {
|
|
28355
|
-
this.options.logger = options.logger;
|
|
28356
|
-
}
|
|
28357
|
-
if (options.httpPorts !== void 0) {
|
|
28358
|
-
this.options.httpPorts = options.httpPorts;
|
|
28359
|
-
}
|
|
28360
|
-
if (options.discoveryMethod !== void 0) {
|
|
28361
|
-
this.options.discoveryMethod = options.discoveryMethod;
|
|
28362
|
-
}
|
|
28363
|
-
if (options.udpBroadcastTimeoutMs !== void 0) {
|
|
28364
|
-
this.options.udpBroadcastTimeoutMs = options.udpBroadcastTimeoutMs;
|
|
28365
|
-
}
|
|
28366
28759
|
if (this.options.autoStart) {
|
|
28367
28760
|
this.start();
|
|
28368
28761
|
}
|
|
28369
28762
|
}
|
|
28370
28763
|
/**
|
|
28371
|
-
*
|
|
28372
|
-
* Se già in esecuzione, non fa nulla.
|
|
28764
|
+
* Start continuous discovery. If already running, does nothing.
|
|
28373
28765
|
*/
|
|
28374
28766
|
start() {
|
|
28375
28767
|
if (this.isRunning) {
|
|
@@ -28384,8 +28776,7 @@ var AutodiscoveryClient = class {
|
|
|
28384
28776
|
this.scheduleNextScan();
|
|
28385
28777
|
}
|
|
28386
28778
|
/**
|
|
28387
|
-
*
|
|
28388
|
-
* Se non è in esecuzione, non fa nulla.
|
|
28779
|
+
* Stop continuous discovery. If not running, does nothing.
|
|
28389
28780
|
*/
|
|
28390
28781
|
stop() {
|
|
28391
28782
|
if (!this.isRunning) {
|
|
@@ -28400,50 +28791,41 @@ var AutodiscoveryClient = class {
|
|
|
28400
28791
|
this.options.logger?.log?.("[Autodiscovery] Discovery stopped");
|
|
28401
28792
|
}
|
|
28402
28793
|
/**
|
|
28403
|
-
*
|
|
28404
|
-
*
|
|
28405
|
-
* @returns Array di dispositivi discoverati, ordinati per host
|
|
28794
|
+
* Returns the current list of discovered devices, sorted by host IP.
|
|
28406
28795
|
*/
|
|
28407
28796
|
getDiscoveredDevices() {
|
|
28408
|
-
return Array.from(this.discoveredDevices.values()).sort(
|
|
28409
|
-
|
|
28410
|
-
|
|
28797
|
+
return Array.from(this.discoveredDevices.values()).sort(
|
|
28798
|
+
(a, b) => a.host.localeCompare(b.host)
|
|
28799
|
+
);
|
|
28411
28800
|
}
|
|
28412
28801
|
/**
|
|
28413
|
-
*
|
|
28414
|
-
*
|
|
28415
|
-
* @returns Numero di dispositivi discoverati
|
|
28802
|
+
* Returns the number of currently discovered devices.
|
|
28416
28803
|
*/
|
|
28417
28804
|
getDeviceCount() {
|
|
28418
28805
|
return this.discoveredDevices.size;
|
|
28419
28806
|
}
|
|
28420
28807
|
/**
|
|
28421
|
-
*
|
|
28422
|
-
*
|
|
28423
|
-
* @returns `true` se il discovery è in esecuzione, `false` altrimenti
|
|
28808
|
+
* Returns whether continuous discovery is currently running.
|
|
28424
28809
|
*/
|
|
28425
28810
|
isActive() {
|
|
28426
28811
|
return this.isRunning;
|
|
28427
28812
|
}
|
|
28428
28813
|
/**
|
|
28429
|
-
*
|
|
28430
|
-
*
|
|
28431
|
-
*
|
|
28432
|
-
* @returns Promise che si risolve quando lo scan è completato
|
|
28814
|
+
* Force an immediate scan (doesn't wait for the scheduled interval).
|
|
28815
|
+
* If a scan is already in progress, waits for it to complete.
|
|
28433
28816
|
*/
|
|
28434
28817
|
async scanNow() {
|
|
28435
28818
|
if (this.currentScanPromise) {
|
|
28436
|
-
this.options.logger?.log?.(
|
|
28819
|
+
this.options.logger?.log?.(
|
|
28820
|
+
"[Autodiscovery] Scan already in progress, waiting for completion..."
|
|
28821
|
+
);
|
|
28437
28822
|
await this.currentScanPromise;
|
|
28438
28823
|
return;
|
|
28439
28824
|
}
|
|
28440
28825
|
await this.performScan();
|
|
28441
28826
|
}
|
|
28442
28827
|
/**
|
|
28443
|
-
*
|
|
28444
|
-
*
|
|
28445
|
-
* @param host - Indirizzo IP del dispositivo da rimuovere
|
|
28446
|
-
* @returns `true` se il dispositivo è stato rimosso, `false` se non era presente
|
|
28828
|
+
* Remove a device from the discovered list.
|
|
28447
28829
|
*/
|
|
28448
28830
|
removeDevice(host) {
|
|
28449
28831
|
const removed = this.discoveredDevices.delete(host);
|
|
@@ -28453,59 +28835,20 @@ var AutodiscoveryClient = class {
|
|
|
28453
28835
|
return removed;
|
|
28454
28836
|
}
|
|
28455
28837
|
/**
|
|
28456
|
-
*
|
|
28838
|
+
* Clear all discovered devices.
|
|
28457
28839
|
*/
|
|
28458
28840
|
clearDevices() {
|
|
28459
28841
|
const count = this.discoveredDevices.size;
|
|
28460
28842
|
this.discoveredDevices.clear();
|
|
28461
|
-
this.options.logger?.log?.(
|
|
28843
|
+
this.options.logger?.log?.(
|
|
28844
|
+
`[Autodiscovery] Removed ${count} device(s) from list`
|
|
28845
|
+
);
|
|
28462
28846
|
}
|
|
28463
|
-
/**
|
|
28464
|
-
* Esegue un singolo scan della rete.
|
|
28465
|
-
*/
|
|
28466
28847
|
async performScan() {
|
|
28467
28848
|
const scanPromise = (async () => {
|
|
28468
28849
|
try {
|
|
28469
28850
|
this.options.logger?.log?.("[Autodiscovery] Starting scan...");
|
|
28470
|
-
const
|
|
28471
|
-
const discoveryOptions = {
|
|
28472
|
-
enableHttpScanning: discoveryMethod === "http" || discoveryMethod === "both",
|
|
28473
|
-
enableUdpDiscovery: discoveryMethod === "udp" || discoveryMethod === "both"
|
|
28474
|
-
};
|
|
28475
|
-
if (this.options.networkCidr !== void 0) {
|
|
28476
|
-
discoveryOptions.networkCidr = this.options.networkCidr;
|
|
28477
|
-
}
|
|
28478
|
-
if (this.options.username !== void 0) {
|
|
28479
|
-
discoveryOptions.username = this.options.username;
|
|
28480
|
-
}
|
|
28481
|
-
if (this.options.password !== void 0) {
|
|
28482
|
-
discoveryOptions.password = this.options.password;
|
|
28483
|
-
}
|
|
28484
|
-
if (this.options.httpProbeTimeoutMs !== void 0) {
|
|
28485
|
-
discoveryOptions.httpProbeTimeoutMs = this.options.httpProbeTimeoutMs;
|
|
28486
|
-
}
|
|
28487
|
-
if (this.options.maxConcurrentProbes !== void 0) {
|
|
28488
|
-
discoveryOptions.maxConcurrentProbes = this.options.maxConcurrentProbes;
|
|
28489
|
-
}
|
|
28490
|
-
if (this.options.logger !== void 0) {
|
|
28491
|
-
discoveryOptions.logger = this.options.logger;
|
|
28492
|
-
}
|
|
28493
|
-
if (this.options.httpPorts !== void 0) {
|
|
28494
|
-
discoveryOptions.httpPorts = this.options.httpPorts;
|
|
28495
|
-
}
|
|
28496
|
-
if (this.options.udpBroadcastTimeoutMs !== void 0) {
|
|
28497
|
-
discoveryOptions.udpBroadcastTimeoutMs = this.options.udpBroadcastTimeoutMs;
|
|
28498
|
-
}
|
|
28499
|
-
let discovered = [];
|
|
28500
|
-
if (discoveryMethod === "http" || discoveryMethod === "both") {
|
|
28501
|
-
const httpDevices = await discoverViaHttpScan(discoveryOptions);
|
|
28502
|
-
discovered.push(...httpDevices);
|
|
28503
|
-
}
|
|
28504
|
-
if (discoveryMethod === "udp" || discoveryMethod === "both") {
|
|
28505
|
-
const udpDevices = await discoverViaUdpBroadcast(discoveryOptions);
|
|
28506
|
-
discovered.push(...udpDevices);
|
|
28507
|
-
}
|
|
28508
|
-
const beforeCount = this.discoveredDevices.size;
|
|
28851
|
+
const discovered = await discoverReolinkDevices(this.options);
|
|
28509
28852
|
const newDevices = [];
|
|
28510
28853
|
const updatedDevices = [];
|
|
28511
28854
|
for (const device of discovered) {
|
|
@@ -28520,38 +28863,35 @@ var AutodiscoveryClient = class {
|
|
|
28520
28863
|
newDevices.push(device);
|
|
28521
28864
|
}
|
|
28522
28865
|
}
|
|
28523
|
-
const afterCount = this.discoveredDevices.size;
|
|
28524
28866
|
this.options.logger?.log?.(
|
|
28525
|
-
`[Autodiscovery] Scan completed: ${newDevices.length} new, ${updatedDevices.length} updated, total: ${
|
|
28867
|
+
`[Autodiscovery] Scan completed: ${newDevices.length} new, ${updatedDevices.length} updated, total: ${this.discoveredDevices.size}`
|
|
28526
28868
|
);
|
|
28527
28869
|
for (const device of newDevices) {
|
|
28528
|
-
const details = [];
|
|
28529
|
-
if (device.model) details.push(`Model: ${device.model}`);
|
|
28530
|
-
if (device.name) details.push(`Name: ${device.name}`);
|
|
28531
|
-
if (device.uid) details.push(`UID: ${device.uid}`);
|
|
28532
|
-
if (device.firmwareVersion) details.push(`Firmware: ${device.firmwareVersion}`);
|
|
28533
|
-
if (device.httpPort) details.push(`HTTP Port: ${device.httpPort}`);
|
|
28534
|
-
if (device.httpsPort) details.push(`HTTPS Port: ${device.httpsPort}`);
|
|
28535
|
-
details.push(`Discovery Method: ${device.discoveryMethod}`);
|
|
28536
|
-
if (device.supportsHttps !== void 0) details.push(`HTTPS: ${device.supportsHttps}`);
|
|
28537
|
-
if (device.httpAccessible !== void 0) details.push(`HTTP Accessible: ${device.httpAccessible}`);
|
|
28538
28870
|
this.options.logger?.log?.(
|
|
28539
|
-
`[Autodiscovery]
|
|
28871
|
+
`[Autodiscovery] NEW DEVICE - ${device.host} | ${device.model ?? "unknown"} | ${device.name ?? ""} | via ${device.discoveryMethod}`
|
|
28540
28872
|
);
|
|
28873
|
+
try {
|
|
28874
|
+
this.options.onDeviceDiscovered?.(device);
|
|
28875
|
+
} catch {
|
|
28876
|
+
}
|
|
28877
|
+
}
|
|
28878
|
+
for (const device of updatedDevices) {
|
|
28879
|
+
try {
|
|
28880
|
+
this.options.onDeviceUpdated?.(device);
|
|
28881
|
+
} catch {
|
|
28882
|
+
}
|
|
28541
28883
|
}
|
|
28542
28884
|
} catch (error) {
|
|
28543
28885
|
const msg = error instanceof Error ? error.message : String(error);
|
|
28544
|
-
this.options.logger?.error?.(
|
|
28886
|
+
this.options.logger?.error?.(
|
|
28887
|
+
`[Autodiscovery] Error during scan: ${msg}`
|
|
28888
|
+
);
|
|
28545
28889
|
}
|
|
28546
28890
|
})();
|
|
28547
28891
|
this.currentScanPromise = scanPromise;
|
|
28548
28892
|
await scanPromise;
|
|
28549
28893
|
this.currentScanPromise = null;
|
|
28550
28894
|
}
|
|
28551
|
-
/**
|
|
28552
|
-
* Unisce le informazioni di un dispositivo esistente con quelle di un nuovo scan.
|
|
28553
|
-
* Restituisce il dispositivo aggiornato se ci sono state modifiche, altrimenti `null`.
|
|
28554
|
-
*/
|
|
28555
28895
|
mergeDeviceInfo(existing, updated) {
|
|
28556
28896
|
let hasChanges = false;
|
|
28557
28897
|
if (!existing.model && updated.model) {
|
|
@@ -28588,13 +28928,8 @@ var AutodiscoveryClient = class {
|
|
|
28588
28928
|
}
|
|
28589
28929
|
return hasChanges ? existing : null;
|
|
28590
28930
|
}
|
|
28591
|
-
/**
|
|
28592
|
-
* Programma il prossimo scan.
|
|
28593
|
-
*/
|
|
28594
28931
|
scheduleNextScan() {
|
|
28595
|
-
if (!this.isRunning)
|
|
28596
|
-
return;
|
|
28597
|
-
}
|
|
28932
|
+
if (!this.isRunning) return;
|
|
28598
28933
|
this.scanTimer = setTimeout(() => {
|
|
28599
28934
|
this.scanTimer = null;
|
|
28600
28935
|
if (this.isRunning) {
|
|
@@ -28633,7 +28968,7 @@ function decideVideoclipTranscodeMode(headers, forceMode) {
|
|
|
28633
28968
|
};
|
|
28634
28969
|
}
|
|
28635
28970
|
const ua = (clientInfo.userAgent ?? "").toLowerCase();
|
|
28636
|
-
const
|
|
28971
|
+
const platform2 = (clientInfo.secChUaPlatform ?? "").toLowerCase().replace(/"/g, "");
|
|
28637
28972
|
const isIos = /iphone|ipad|ipod/.test(ua);
|
|
28638
28973
|
if (isIos) {
|
|
28639
28974
|
return {
|
|
@@ -28650,7 +28985,7 @@ function decideVideoclipTranscodeMode(headers, forceMode) {
|
|
|
28650
28985
|
clientInfo
|
|
28651
28986
|
};
|
|
28652
28987
|
}
|
|
28653
|
-
const isAndroid = ua.includes("android") ||
|
|
28988
|
+
const isAndroid = ua.includes("android") || platform2 === "android";
|
|
28654
28989
|
if (isAndroid) {
|
|
28655
28990
|
return {
|
|
28656
28991
|
mode: "transcode-h264",
|
|
@@ -28659,7 +28994,7 @@ function decideVideoclipTranscodeMode(headers, forceMode) {
|
|
|
28659
28994
|
};
|
|
28660
28995
|
}
|
|
28661
28996
|
const isChromium = ua.includes("chrome") || ua.includes("edg");
|
|
28662
|
-
const isMac = ua.includes("mac os") ||
|
|
28997
|
+
const isMac = ua.includes("mac os") || platform2 === "macos";
|
|
28663
28998
|
if (isChromium && !isMac) {
|
|
28664
28999
|
return {
|
|
28665
29000
|
mode: "transcode-h264",
|
|
@@ -28686,7 +29021,7 @@ init_recordingFileName();
|
|
|
28686
29021
|
|
|
28687
29022
|
// src/reolink/baichuan/endpoints-server.ts
|
|
28688
29023
|
var import_node_http = __toESM(require("http"), 1);
|
|
28689
|
-
var
|
|
29024
|
+
var import_node_child_process5 = require("child_process");
|
|
28690
29025
|
function parseIntParam(v, def) {
|
|
28691
29026
|
if (v == null) return def;
|
|
28692
29027
|
const n = Number.parseInt(v, 10);
|
|
@@ -28925,7 +29260,7 @@ function createBaichuanEndpointsServer(opts) {
|
|
|
28925
29260
|
"Cache-Control": "no-cache",
|
|
28926
29261
|
Connection: "close"
|
|
28927
29262
|
});
|
|
28928
|
-
const ff2 = (0,
|
|
29263
|
+
const ff2 = (0, import_node_child_process5.spawn)("ffmpeg", [
|
|
28929
29264
|
"-hide_banner",
|
|
28930
29265
|
"-loglevel",
|
|
28931
29266
|
"error",
|
|
@@ -28958,7 +29293,7 @@ function createBaichuanEndpointsServer(opts) {
|
|
|
28958
29293
|
);
|
|
28959
29294
|
res.setHeader("Cache-Control", "no-cache");
|
|
28960
29295
|
res.setHeader("Connection", "close");
|
|
28961
|
-
const ff = (0,
|
|
29296
|
+
const ff = (0, import_node_child_process5.spawn)("ffmpeg", [
|
|
28962
29297
|
"-hide_banner",
|
|
28963
29298
|
"-loglevel",
|
|
28964
29299
|
"error",
|
|
@@ -29069,7 +29404,7 @@ init_urls();
|
|
|
29069
29404
|
|
|
29070
29405
|
// src/rtsp/server.ts
|
|
29071
29406
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
29072
|
-
var
|
|
29407
|
+
var import_node_child_process6 = require("child_process");
|
|
29073
29408
|
init_urls();
|
|
29074
29409
|
function createRtspProxyServer(opts) {
|
|
29075
29410
|
return import_node_http2.default.createServer((req, res) => {
|
|
@@ -29110,7 +29445,7 @@ function createRtspProxyServer(opts) {
|
|
|
29110
29445
|
Connection: "close"
|
|
29111
29446
|
});
|
|
29112
29447
|
const rtspTransport = opts.rtspTransport ?? "tcp";
|
|
29113
|
-
const ff = (0,
|
|
29448
|
+
const ff = (0, import_node_child_process6.spawn)("ffmpeg", [
|
|
29114
29449
|
"-hide_banner",
|
|
29115
29450
|
"-loglevel",
|
|
29116
29451
|
"error",
|
|
@@ -29140,7 +29475,7 @@ function createRtspProxyServer(opts) {
|
|
|
29140
29475
|
}
|
|
29141
29476
|
|
|
29142
29477
|
// src/rfc/rfc4571.ts
|
|
29143
|
-
var
|
|
29478
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
29144
29479
|
function buildRfc4571Sdp(video, audio) {
|
|
29145
29480
|
let out = "v=0\r\n";
|
|
29146
29481
|
out += "o=- 0 0 IN IP4 0.0.0.0\r\n";
|
|
@@ -29413,12 +29748,12 @@ function parseAdtsHeader(adtsFrame) {
|
|
|
29413
29748
|
var RtpWriter = class {
|
|
29414
29749
|
constructor(payloadType) {
|
|
29415
29750
|
this.payloadType = payloadType;
|
|
29416
|
-
this.seq =
|
|
29417
|
-
this.timestamp =
|
|
29751
|
+
this.seq = import_node_crypto4.default.randomBytes(2).readUInt16BE(0);
|
|
29752
|
+
this.timestamp = import_node_crypto4.default.randomBytes(4).readUInt32BE(0);
|
|
29418
29753
|
}
|
|
29419
29754
|
seq = 0;
|
|
29420
29755
|
timestamp = 0;
|
|
29421
|
-
ssrc =
|
|
29756
|
+
ssrc = import_node_crypto4.default.randomBytes(4).readUInt32BE(0);
|
|
29422
29757
|
setTimestamp(ts) {
|
|
29423
29758
|
this.timestamp = ts >>> 0;
|
|
29424
29759
|
}
|
|
@@ -29982,8 +30317,8 @@ var import_node_net2 = __toESM(require("net"), 1);
|
|
|
29982
30317
|
init_BaichuanVideoStream();
|
|
29983
30318
|
|
|
29984
30319
|
// src/multifocal/compositeStream.ts
|
|
29985
|
-
var
|
|
29986
|
-
var
|
|
30320
|
+
var import_node_child_process7 = require("child_process");
|
|
30321
|
+
var import_node_crypto5 = require("crypto");
|
|
29987
30322
|
var import_node_events5 = require("events");
|
|
29988
30323
|
function calculateOverlayPosition(position, mainWidth, mainHeight, pipWidth, pipHeight, margin) {
|
|
29989
30324
|
const pipW = Math.floor(pipWidth);
|
|
@@ -30094,7 +30429,7 @@ var CompositeStream = class extends import_node_events5.EventEmitter {
|
|
|
30094
30429
|
if (!buf?.length) return;
|
|
30095
30430
|
const head = buf.subarray(0, Math.min(12, buf.length)).toString("hex");
|
|
30096
30431
|
const slice = buf.subarray(0, Math.min(256, buf.length));
|
|
30097
|
-
const sha1 = (0,
|
|
30432
|
+
const sha1 = (0, import_node_crypto5.createHash)("sha1").update(slice).digest("hex");
|
|
30098
30433
|
return { len: buf.length, headHex: head, sha1_256: sha1 };
|
|
30099
30434
|
}
|
|
30100
30435
|
async primeForFfmpeg(gen, timeoutMs, requireKeyframe) {
|
|
@@ -30323,7 +30658,7 @@ var CompositeStream = class extends import_node_events5.EventEmitter {
|
|
|
30323
30658
|
"pipe:1"
|
|
30324
30659
|
];
|
|
30325
30660
|
this.logger.log?.(`[CompositeStream] Starting ffmpeg (rtsp inputs): ${ffmpegArgs.join(" ")}`);
|
|
30326
|
-
this.ffmpegProcess = (0,
|
|
30661
|
+
this.ffmpegProcess = (0, import_node_child_process7.spawn)("ffmpeg", ffmpegArgs, {
|
|
30327
30662
|
stdio: ["ignore", "pipe", "pipe"]
|
|
30328
30663
|
});
|
|
30329
30664
|
this.ffmpegProcess.on("error", (error) => {
|
|
@@ -30438,7 +30773,7 @@ var CompositeStream = class extends import_node_events5.EventEmitter {
|
|
|
30438
30773
|
this.logger.log?.(
|
|
30439
30774
|
`[CompositeStream] Starting ffmpeg: ${ffmpegArgs.join(" ")}`
|
|
30440
30775
|
);
|
|
30441
|
-
this.ffmpegProcess = (0,
|
|
30776
|
+
this.ffmpegProcess = (0, import_node_child_process7.spawn)("ffmpeg", ffmpegArgs, {
|
|
30442
30777
|
stdio: ["pipe", "pipe", "pipe", "pipe"]
|
|
30443
30778
|
});
|
|
30444
30779
|
this.ffmpegProcess.on("error", (error) => {
|
|
@@ -32167,7 +32502,7 @@ async function createRfc4571TcpServerForReplay(options) {
|
|
|
32167
32502
|
|
|
32168
32503
|
// src/rfc/replay-http-server.ts
|
|
32169
32504
|
var import_node_http3 = __toESM(require("http"), 1);
|
|
32170
|
-
var
|
|
32505
|
+
var import_node_child_process8 = require("child_process");
|
|
32171
32506
|
var import_node_stream2 = require("stream");
|
|
32172
32507
|
async function createReplayHttpServer(options) {
|
|
32173
32508
|
const {
|
|
@@ -32321,7 +32656,7 @@ async function createReplayHttpServer(options) {
|
|
|
32321
32656
|
"pipe:1"
|
|
32322
32657
|
];
|
|
32323
32658
|
log(`spawning ffmpeg: ${ffmpegPath} ${ffmpegArgs.join(" ")}`);
|
|
32324
|
-
ffmpegProcess = (0,
|
|
32659
|
+
ffmpegProcess = (0, import_node_child_process8.spawn)(ffmpegPath, ffmpegArgs, {
|
|
32325
32660
|
stdio: ["pipe", "pipe", "pipe"]
|
|
32326
32661
|
});
|
|
32327
32662
|
ffmpegProcess.stdout?.pipe(outputStream).pipe(res);
|
|
@@ -32421,34 +32756,555 @@ async function createReplayHttpServer(options) {
|
|
|
32421
32756
|
// src/index.ts
|
|
32422
32757
|
init_BaichuanVideoStream();
|
|
32423
32758
|
|
|
32424
|
-
// src/baichuan/stream/
|
|
32759
|
+
// src/baichuan/stream/Go2rtcTcpServer.ts
|
|
32425
32760
|
var import_node_events6 = require("events");
|
|
32426
|
-
var
|
|
32427
|
-
|
|
32428
|
-
|
|
32429
|
-
var
|
|
32430
|
-
|
|
32431
|
-
|
|
32432
|
-
|
|
32433
|
-
|
|
32434
|
-
|
|
32435
|
-
|
|
32436
|
-
for (let i = 0; i < annexB.length - 3; i++) {
|
|
32437
|
-
if (annexB[i] === 0 && annexB[i + 1] === 0) {
|
|
32438
|
-
if (annexB[i + 2] === 1) {
|
|
32439
|
-
starts.push({ idx: i, len: 3 });
|
|
32440
|
-
i += 2;
|
|
32441
|
-
} else if (annexB[i + 2] === 0 && annexB[i + 3] === 1) {
|
|
32442
|
-
starts.push({ idx: i, len: 4 });
|
|
32443
|
-
i += 3;
|
|
32444
|
-
}
|
|
32445
|
-
}
|
|
32761
|
+
var net4 = __toESM(require("net"), 1);
|
|
32762
|
+
init_H264Converter();
|
|
32763
|
+
init_H265Converter();
|
|
32764
|
+
var AsyncBoundedQueue2 = class {
|
|
32765
|
+
maxItems;
|
|
32766
|
+
queue = [];
|
|
32767
|
+
waiting;
|
|
32768
|
+
closed = false;
|
|
32769
|
+
constructor(maxItems) {
|
|
32770
|
+
this.maxItems = Math.max(1, maxItems | 0);
|
|
32446
32771
|
}
|
|
32447
|
-
|
|
32448
|
-
|
|
32449
|
-
|
|
32450
|
-
|
|
32451
|
-
|
|
32772
|
+
push(item) {
|
|
32773
|
+
if (this.closed) return;
|
|
32774
|
+
if (this.waiting) {
|
|
32775
|
+
const { resolve } = this.waiting;
|
|
32776
|
+
this.waiting = void 0;
|
|
32777
|
+
resolve({ value: item, done: false });
|
|
32778
|
+
return;
|
|
32779
|
+
}
|
|
32780
|
+
this.queue.push(item);
|
|
32781
|
+
if (this.queue.length > this.maxItems) {
|
|
32782
|
+
this.queue.splice(0, this.queue.length - this.maxItems);
|
|
32783
|
+
}
|
|
32784
|
+
}
|
|
32785
|
+
close() {
|
|
32786
|
+
if (this.closed) return;
|
|
32787
|
+
this.closed = true;
|
|
32788
|
+
if (this.waiting) {
|
|
32789
|
+
const { resolve } = this.waiting;
|
|
32790
|
+
this.waiting = void 0;
|
|
32791
|
+
resolve({ value: void 0, done: true });
|
|
32792
|
+
}
|
|
32793
|
+
}
|
|
32794
|
+
async next() {
|
|
32795
|
+
if (this.closed) return { value: void 0, done: true };
|
|
32796
|
+
const item = this.queue.shift();
|
|
32797
|
+
if (item !== void 0) return { value: item, done: false };
|
|
32798
|
+
return await new Promise((resolve) => {
|
|
32799
|
+
this.waiting = { resolve };
|
|
32800
|
+
});
|
|
32801
|
+
}
|
|
32802
|
+
};
|
|
32803
|
+
var NativeStreamFanout2 = class {
|
|
32804
|
+
opts;
|
|
32805
|
+
queues = /* @__PURE__ */ new Map();
|
|
32806
|
+
source = null;
|
|
32807
|
+
running = false;
|
|
32808
|
+
pumpPromise = null;
|
|
32809
|
+
constructor(opts) {
|
|
32810
|
+
this.opts = opts;
|
|
32811
|
+
}
|
|
32812
|
+
start() {
|
|
32813
|
+
if (this.running) return;
|
|
32814
|
+
this.running = true;
|
|
32815
|
+
this.source = this.opts.createSource();
|
|
32816
|
+
this.pumpPromise = (async () => {
|
|
32817
|
+
try {
|
|
32818
|
+
for await (const frame of this.source) {
|
|
32819
|
+
try {
|
|
32820
|
+
this.opts.onFrame?.(frame);
|
|
32821
|
+
} catch {
|
|
32822
|
+
}
|
|
32823
|
+
for (const q of this.queues.values()) {
|
|
32824
|
+
q.push(frame);
|
|
32825
|
+
}
|
|
32826
|
+
}
|
|
32827
|
+
} catch (e) {
|
|
32828
|
+
this.opts.onError?.(e);
|
|
32829
|
+
} finally {
|
|
32830
|
+
for (const q of this.queues.values()) q.close();
|
|
32831
|
+
this.queues.clear();
|
|
32832
|
+
this.running = false;
|
|
32833
|
+
this.opts.onEnd?.();
|
|
32834
|
+
}
|
|
32835
|
+
})();
|
|
32836
|
+
}
|
|
32837
|
+
subscribe(id) {
|
|
32838
|
+
const q = new AsyncBoundedQueue2(this.opts.maxQueueItems);
|
|
32839
|
+
this.queues.set(id, q);
|
|
32840
|
+
const self = this;
|
|
32841
|
+
return (async function* () {
|
|
32842
|
+
try {
|
|
32843
|
+
while (true) {
|
|
32844
|
+
const r = await q.next();
|
|
32845
|
+
if (r.done) return;
|
|
32846
|
+
yield r.value;
|
|
32847
|
+
}
|
|
32848
|
+
} finally {
|
|
32849
|
+
q.close();
|
|
32850
|
+
self.queues.delete(id);
|
|
32851
|
+
}
|
|
32852
|
+
})();
|
|
32853
|
+
}
|
|
32854
|
+
async stop() {
|
|
32855
|
+
if (!this.running) return;
|
|
32856
|
+
this.running = false;
|
|
32857
|
+
const src = this.source;
|
|
32858
|
+
this.source = null;
|
|
32859
|
+
for (const q of this.queues.values()) q.close();
|
|
32860
|
+
this.queues.clear();
|
|
32861
|
+
try {
|
|
32862
|
+
await src?.return(void 0);
|
|
32863
|
+
} catch {
|
|
32864
|
+
}
|
|
32865
|
+
try {
|
|
32866
|
+
await this.pumpPromise;
|
|
32867
|
+
} catch {
|
|
32868
|
+
}
|
|
32869
|
+
this.pumpPromise = null;
|
|
32870
|
+
}
|
|
32871
|
+
};
|
|
32872
|
+
var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEmitter {
|
|
32873
|
+
api;
|
|
32874
|
+
channel;
|
|
32875
|
+
profile;
|
|
32876
|
+
variant;
|
|
32877
|
+
listenHost;
|
|
32878
|
+
listenPort;
|
|
32879
|
+
logger;
|
|
32880
|
+
deviceId;
|
|
32881
|
+
gracePeriodMs;
|
|
32882
|
+
prebufferMaxMs;
|
|
32883
|
+
maxBufferBytes;
|
|
32884
|
+
prestartStream;
|
|
32885
|
+
active = false;
|
|
32886
|
+
server;
|
|
32887
|
+
resolvedPort;
|
|
32888
|
+
// Native stream
|
|
32889
|
+
nativeFanout = null;
|
|
32890
|
+
nativeStreamActive = false;
|
|
32891
|
+
dedicatedSessionRelease;
|
|
32892
|
+
detectedVideoType;
|
|
32893
|
+
// Client tracking
|
|
32894
|
+
connectedClients = /* @__PURE__ */ new Set();
|
|
32895
|
+
clientSockets = /* @__PURE__ */ new Map();
|
|
32896
|
+
stopGraceTimer;
|
|
32897
|
+
// Prebuffer
|
|
32898
|
+
prebuffer = [];
|
|
32899
|
+
constructor(options) {
|
|
32900
|
+
super();
|
|
32901
|
+
this.api = options.api;
|
|
32902
|
+
this.channel = options.channel;
|
|
32903
|
+
this.profile = options.profile;
|
|
32904
|
+
this.variant = options.variant ?? "default";
|
|
32905
|
+
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
32906
|
+
this.listenPort = options.listenPort ?? 0;
|
|
32907
|
+
this.logger = options.logger ?? console;
|
|
32908
|
+
this.deviceId = options.deviceId;
|
|
32909
|
+
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
32910
|
+
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
32911
|
+
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
32912
|
+
this.prestartStream = options.prestartStream ?? true;
|
|
32913
|
+
}
|
|
32914
|
+
// -----------------------------------------------------------------------
|
|
32915
|
+
// Public API
|
|
32916
|
+
// -----------------------------------------------------------------------
|
|
32917
|
+
/** Start listening. Resolves once the TCP server is bound. */
|
|
32918
|
+
async start() {
|
|
32919
|
+
if (this.active) return;
|
|
32920
|
+
this.active = true;
|
|
32921
|
+
this.server = net4.createServer((socket) => this.handleClient(socket));
|
|
32922
|
+
this.server.on("error", (err) => {
|
|
32923
|
+
this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
|
|
32924
|
+
this.emit("error", err);
|
|
32925
|
+
});
|
|
32926
|
+
await new Promise((resolve, reject) => {
|
|
32927
|
+
this.server.listen(this.listenPort, this.listenHost, () => {
|
|
32928
|
+
const addr = this.server.address();
|
|
32929
|
+
this.resolvedPort = addr.port;
|
|
32930
|
+
this.logger.info?.(
|
|
32931
|
+
`[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
|
|
32932
|
+
);
|
|
32933
|
+
this.emit("listening", { host: addr.address, port: addr.port });
|
|
32934
|
+
resolve();
|
|
32935
|
+
});
|
|
32936
|
+
this.server.once("error", reject);
|
|
32937
|
+
});
|
|
32938
|
+
if (this.prestartStream) {
|
|
32939
|
+
this.logger.info?.(
|
|
32940
|
+
`[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
|
|
32941
|
+
);
|
|
32942
|
+
this.startNativeStream();
|
|
32943
|
+
}
|
|
32944
|
+
}
|
|
32945
|
+
/** Stop the server and all active streams. */
|
|
32946
|
+
async stop() {
|
|
32947
|
+
if (!this.active) return;
|
|
32948
|
+
this.active = false;
|
|
32949
|
+
clearTimeout(this.stopGraceTimer);
|
|
32950
|
+
for (const [id, sock] of this.clientSockets) {
|
|
32951
|
+
sock.destroy();
|
|
32952
|
+
this.connectedClients.delete(id);
|
|
32953
|
+
}
|
|
32954
|
+
this.clientSockets.clear();
|
|
32955
|
+
await this.stopNativeStream();
|
|
32956
|
+
if (this.server) {
|
|
32957
|
+
await new Promise((resolve) => {
|
|
32958
|
+
this.server.close(() => resolve());
|
|
32959
|
+
});
|
|
32960
|
+
this.server = void 0;
|
|
32961
|
+
}
|
|
32962
|
+
this.prebuffer = [];
|
|
32963
|
+
this.resolvedPort = void 0;
|
|
32964
|
+
this.emit("close");
|
|
32965
|
+
}
|
|
32966
|
+
/** The actual port the server is listening on (available after start()). */
|
|
32967
|
+
get port() {
|
|
32968
|
+
return this.resolvedPort;
|
|
32969
|
+
}
|
|
32970
|
+
/** The go2rtc-compatible source URL. */
|
|
32971
|
+
get go2rtcSourceUrl() {
|
|
32972
|
+
if (this.resolvedPort == null) return void 0;
|
|
32973
|
+
return `tcp://127.0.0.1:${this.resolvedPort}`;
|
|
32974
|
+
}
|
|
32975
|
+
/** Number of currently connected clients. */
|
|
32976
|
+
get clientCount() {
|
|
32977
|
+
return this.connectedClients.size;
|
|
32978
|
+
}
|
|
32979
|
+
// -----------------------------------------------------------------------
|
|
32980
|
+
// Client handling
|
|
32981
|
+
// -----------------------------------------------------------------------
|
|
32982
|
+
handleClient(socket) {
|
|
32983
|
+
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
32984
|
+
socket.setNoDelay(true);
|
|
32985
|
+
this.connectedClients.add(clientId);
|
|
32986
|
+
this.clientSockets.set(clientId, socket);
|
|
32987
|
+
this.logger.info?.(
|
|
32988
|
+
`[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
|
|
32989
|
+
);
|
|
32990
|
+
this.emit("client", clientId);
|
|
32991
|
+
if (this.stopGraceTimer) {
|
|
32992
|
+
clearTimeout(this.stopGraceTimer);
|
|
32993
|
+
this.stopGraceTimer = void 0;
|
|
32994
|
+
}
|
|
32995
|
+
if (!this.nativeStreamActive) {
|
|
32996
|
+
this.startNativeStream();
|
|
32997
|
+
}
|
|
32998
|
+
this.feedClient(clientId, socket).catch((err) => {
|
|
32999
|
+
this.logger.warn?.(
|
|
33000
|
+
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
33001
|
+
);
|
|
33002
|
+
});
|
|
33003
|
+
const cleanup = () => {
|
|
33004
|
+
this.removeClient(clientId);
|
|
33005
|
+
socket.destroy();
|
|
33006
|
+
};
|
|
33007
|
+
socket.on("error", cleanup);
|
|
33008
|
+
socket.on("close", cleanup);
|
|
33009
|
+
}
|
|
33010
|
+
async feedClient(clientId, socket) {
|
|
33011
|
+
const fanoutDeadline = Date.now() + 3e4;
|
|
33012
|
+
while (this.active && !this.nativeFanout) {
|
|
33013
|
+
if (socket.destroyed) return;
|
|
33014
|
+
if (Date.now() > fanoutDeadline) {
|
|
33015
|
+
this.logger.warn?.(
|
|
33016
|
+
`[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
|
|
33017
|
+
);
|
|
33018
|
+
return;
|
|
33019
|
+
}
|
|
33020
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
33021
|
+
}
|
|
33022
|
+
if (!this.active || !this.nativeFanout) return;
|
|
33023
|
+
const subscription = this.nativeFanout.subscribe(clientId);
|
|
33024
|
+
const prebufferSnap = this.prebuffer.slice();
|
|
33025
|
+
let lastIdrIdx = -1;
|
|
33026
|
+
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
33027
|
+
if (prebufferSnap[i].isKeyframe) {
|
|
33028
|
+
lastIdrIdx = i;
|
|
33029
|
+
break;
|
|
33030
|
+
}
|
|
33031
|
+
}
|
|
33032
|
+
if (lastIdrIdx >= 0) {
|
|
33033
|
+
const replay = prebufferSnap.slice(lastIdrIdx);
|
|
33034
|
+
this.logger.info?.(
|
|
33035
|
+
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
33036
|
+
);
|
|
33037
|
+
for (const entry of replay) {
|
|
33038
|
+
if (socket.destroyed) return;
|
|
33039
|
+
socket.write(entry.data);
|
|
33040
|
+
}
|
|
33041
|
+
}
|
|
33042
|
+
let seenKeyframe = lastIdrIdx >= 0;
|
|
33043
|
+
let liveFrameCount = 0;
|
|
33044
|
+
let liveVideoWritten = 0;
|
|
33045
|
+
let lastLogAt = Date.now();
|
|
33046
|
+
try {
|
|
33047
|
+
this.logger.info?.(
|
|
33048
|
+
`[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
|
|
33049
|
+
);
|
|
33050
|
+
for await (const frame of subscription) {
|
|
33051
|
+
if (socket.destroyed || !this.active) {
|
|
33052
|
+
this.logger.info?.(
|
|
33053
|
+
`[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
|
|
33054
|
+
);
|
|
33055
|
+
break;
|
|
33056
|
+
}
|
|
33057
|
+
liveFrameCount++;
|
|
33058
|
+
const annexB = this.convertFrame(frame);
|
|
33059
|
+
if (!annexB) continue;
|
|
33060
|
+
if (!seenKeyframe) {
|
|
33061
|
+
if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
|
|
33062
|
+
seenKeyframe = true;
|
|
33063
|
+
this.logger.info?.(
|
|
33064
|
+
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
33065
|
+
);
|
|
33066
|
+
}
|
|
33067
|
+
socket.write(annexB);
|
|
33068
|
+
liveVideoWritten++;
|
|
33069
|
+
if (Date.now() - lastLogAt > 1e4) {
|
|
33070
|
+
this.logger.info?.(
|
|
33071
|
+
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
33072
|
+
);
|
|
33073
|
+
lastLogAt = Date.now();
|
|
33074
|
+
}
|
|
33075
|
+
if (socket.writableLength > this.maxBufferBytes) {
|
|
33076
|
+
this.logger.warn?.(
|
|
33077
|
+
`[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
|
|
33078
|
+
);
|
|
33079
|
+
socket.destroy();
|
|
33080
|
+
break;
|
|
33081
|
+
}
|
|
33082
|
+
}
|
|
33083
|
+
this.logger.info?.(
|
|
33084
|
+
`[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
|
|
33085
|
+
);
|
|
33086
|
+
} finally {
|
|
33087
|
+
await subscription.return(void 0).catch(() => {
|
|
33088
|
+
});
|
|
33089
|
+
}
|
|
33090
|
+
}
|
|
33091
|
+
// -----------------------------------------------------------------------
|
|
33092
|
+
// Frame conversion
|
|
33093
|
+
// -----------------------------------------------------------------------
|
|
33094
|
+
/**
|
|
33095
|
+
* Convert a native frame to wire-ready Annex-B.
|
|
33096
|
+
* Audio frames are skipped — raw TCP carries only video (Annex-B).
|
|
33097
|
+
* go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
|
|
33098
|
+
*/
|
|
33099
|
+
convertFrame(frame) {
|
|
33100
|
+
if (frame.audio) {
|
|
33101
|
+
return null;
|
|
33102
|
+
}
|
|
33103
|
+
if (frame.data.length === 0) return null;
|
|
33104
|
+
try {
|
|
33105
|
+
if (frame.videoType === "H264") {
|
|
33106
|
+
return convertToAnnexB(frame.data);
|
|
33107
|
+
}
|
|
33108
|
+
if (frame.videoType === "H265") {
|
|
33109
|
+
return convertToAnnexB2(frame.data);
|
|
33110
|
+
}
|
|
33111
|
+
} catch {
|
|
33112
|
+
}
|
|
33113
|
+
return frame.data;
|
|
33114
|
+
}
|
|
33115
|
+
/** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
|
|
33116
|
+
isAnnexBKeyframe(annexB, videoType) {
|
|
33117
|
+
try {
|
|
33118
|
+
if (videoType === "H264") {
|
|
33119
|
+
const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
|
|
33120
|
+
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
33121
|
+
}
|
|
33122
|
+
if (videoType === "H265") {
|
|
33123
|
+
const nals = splitAnnexBToNalPayloads2(annexB);
|
|
33124
|
+
return nals.some(
|
|
33125
|
+
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
33126
|
+
);
|
|
33127
|
+
}
|
|
33128
|
+
} catch {
|
|
33129
|
+
}
|
|
33130
|
+
return false;
|
|
33131
|
+
}
|
|
33132
|
+
/** Split Annex-B byte stream into individual NAL units. */
|
|
33133
|
+
static splitAnnexBNals(buf) {
|
|
33134
|
+
const nals = [];
|
|
33135
|
+
let i = 0;
|
|
33136
|
+
while (i < buf.length) {
|
|
33137
|
+
if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
|
|
33138
|
+
let scLen;
|
|
33139
|
+
if (buf[i + 2] === 1) {
|
|
33140
|
+
scLen = 3;
|
|
33141
|
+
} else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
|
|
33142
|
+
scLen = 4;
|
|
33143
|
+
} else {
|
|
33144
|
+
i++;
|
|
33145
|
+
continue;
|
|
33146
|
+
}
|
|
33147
|
+
const nalStart = i + scLen;
|
|
33148
|
+
let nalEnd = buf.length;
|
|
33149
|
+
for (let j = nalStart; j < buf.length - 2; j++) {
|
|
33150
|
+
if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
|
|
33151
|
+
nalEnd = j;
|
|
33152
|
+
break;
|
|
33153
|
+
}
|
|
33154
|
+
}
|
|
33155
|
+
if (nalEnd > nalStart) {
|
|
33156
|
+
nals.push(buf.subarray(nalStart, nalEnd));
|
|
33157
|
+
}
|
|
33158
|
+
i = nalEnd;
|
|
33159
|
+
} else {
|
|
33160
|
+
i++;
|
|
33161
|
+
}
|
|
33162
|
+
}
|
|
33163
|
+
return nals;
|
|
33164
|
+
}
|
|
33165
|
+
// -----------------------------------------------------------------------
|
|
33166
|
+
// Native stream management
|
|
33167
|
+
// -----------------------------------------------------------------------
|
|
33168
|
+
async startNativeStream() {
|
|
33169
|
+
if (this.nativeStreamActive) return;
|
|
33170
|
+
this.nativeStreamActive = true;
|
|
33171
|
+
let dedicatedClient;
|
|
33172
|
+
if (this.deviceId) {
|
|
33173
|
+
try {
|
|
33174
|
+
const session = await this.api.createDedicatedSession(
|
|
33175
|
+
`live:${this.deviceId}:ch${this.channel}:${this.profile}`
|
|
33176
|
+
);
|
|
33177
|
+
dedicatedClient = session.client;
|
|
33178
|
+
this.dedicatedSessionRelease = session.release;
|
|
33179
|
+
} catch (e) {
|
|
33180
|
+
this.logger.warn?.(
|
|
33181
|
+
`[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
|
|
33182
|
+
);
|
|
33183
|
+
}
|
|
33184
|
+
}
|
|
33185
|
+
this.logger.info?.(
|
|
33186
|
+
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
33187
|
+
);
|
|
33188
|
+
this.nativeFanout = new NativeStreamFanout2({
|
|
33189
|
+
maxQueueItems: 200,
|
|
33190
|
+
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
33191
|
+
variant: this.variant,
|
|
33192
|
+
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
33193
|
+
}),
|
|
33194
|
+
onFrame: (frame) => {
|
|
33195
|
+
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
33196
|
+
this.detectedVideoType = frame.videoType;
|
|
33197
|
+
}
|
|
33198
|
+
const wireData = this.convertFrame(frame);
|
|
33199
|
+
if (!wireData || wireData.length === 0) return;
|
|
33200
|
+
const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
|
|
33201
|
+
this.prebuffer.push({
|
|
33202
|
+
data: Buffer.from(wireData),
|
|
33203
|
+
time: Date.now(),
|
|
33204
|
+
isKeyframe,
|
|
33205
|
+
audio: frame.audio
|
|
33206
|
+
});
|
|
33207
|
+
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
33208
|
+
let trimIdx = 0;
|
|
33209
|
+
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
33210
|
+
trimIdx++;
|
|
33211
|
+
}
|
|
33212
|
+
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
33213
|
+
},
|
|
33214
|
+
onError: (error) => {
|
|
33215
|
+
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
33216
|
+
},
|
|
33217
|
+
onEnd: () => {
|
|
33218
|
+
if (!this.nativeStreamActive) return;
|
|
33219
|
+
this.nativeStreamActive = false;
|
|
33220
|
+
this.nativeFanout = null;
|
|
33221
|
+
if (this.dedicatedSessionRelease) {
|
|
33222
|
+
this.dedicatedSessionRelease().catch(() => {
|
|
33223
|
+
});
|
|
33224
|
+
this.dedicatedSessionRelease = void 0;
|
|
33225
|
+
}
|
|
33226
|
+
if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
|
|
33227
|
+
this.logger.info?.(
|
|
33228
|
+
`[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
33229
|
+
);
|
|
33230
|
+
this.startNativeStream();
|
|
33231
|
+
}
|
|
33232
|
+
}
|
|
33233
|
+
});
|
|
33234
|
+
this.nativeFanout.start();
|
|
33235
|
+
}
|
|
33236
|
+
async stopNativeStream() {
|
|
33237
|
+
this.nativeStreamActive = false;
|
|
33238
|
+
const fanout = this.nativeFanout;
|
|
33239
|
+
this.nativeFanout = null;
|
|
33240
|
+
if (fanout) {
|
|
33241
|
+
await fanout.stop();
|
|
33242
|
+
}
|
|
33243
|
+
this.prebuffer = [];
|
|
33244
|
+
if (this.dedicatedSessionRelease) {
|
|
33245
|
+
await this.dedicatedSessionRelease().catch(() => {
|
|
33246
|
+
});
|
|
33247
|
+
this.dedicatedSessionRelease = void 0;
|
|
33248
|
+
}
|
|
33249
|
+
}
|
|
33250
|
+
// -----------------------------------------------------------------------
|
|
33251
|
+
// Client lifecycle
|
|
33252
|
+
// -----------------------------------------------------------------------
|
|
33253
|
+
removeClient(clientId) {
|
|
33254
|
+
if (!this.connectedClients.has(clientId)) return;
|
|
33255
|
+
this.connectedClients.delete(clientId);
|
|
33256
|
+
this.clientSockets.delete(clientId);
|
|
33257
|
+
this.logger.info?.(
|
|
33258
|
+
`[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
|
|
33259
|
+
);
|
|
33260
|
+
this.emit("clientDisconnected", clientId);
|
|
33261
|
+
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
33262
|
+
this.scheduleStop();
|
|
33263
|
+
}
|
|
33264
|
+
}
|
|
33265
|
+
scheduleStop() {
|
|
33266
|
+
if (this.stopGraceTimer) return;
|
|
33267
|
+
this.logger.info?.(
|
|
33268
|
+
`[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
|
|
33269
|
+
);
|
|
33270
|
+
this.stopGraceTimer = setTimeout(async () => {
|
|
33271
|
+
this.stopGraceTimer = void 0;
|
|
33272
|
+
if (this.connectedClients.size === 0 && this.nativeStreamActive) {
|
|
33273
|
+
this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
|
|
33274
|
+
await this.stopNativeStream();
|
|
33275
|
+
}
|
|
33276
|
+
}, this.gracePeriodMs);
|
|
33277
|
+
}
|
|
33278
|
+
};
|
|
33279
|
+
|
|
33280
|
+
// src/baichuan/stream/BaichuanHttpStreamServer.ts
|
|
33281
|
+
var import_node_events7 = require("events");
|
|
33282
|
+
var import_node_child_process9 = require("child_process");
|
|
33283
|
+
var http4 = __toESM(require("http"), 1);
|
|
33284
|
+
var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
|
|
33285
|
+
var NAL_START_CODE_3B3 = Buffer.from([0, 0, 1]);
|
|
33286
|
+
function hasAnnexBStart(data) {
|
|
33287
|
+
if (data.length < 4) return false;
|
|
33288
|
+
return data.subarray(0, 4).equals(NAL_START_CODE_4B4) || data.subarray(0, 3).equals(NAL_START_CODE_3B3);
|
|
33289
|
+
}
|
|
33290
|
+
function splitAnnexBNals(annexB) {
|
|
33291
|
+
const starts = [];
|
|
33292
|
+
for (let i = 0; i < annexB.length - 3; i++) {
|
|
33293
|
+
if (annexB[i] === 0 && annexB[i + 1] === 0) {
|
|
33294
|
+
if (annexB[i + 2] === 1) {
|
|
33295
|
+
starts.push({ idx: i, len: 3 });
|
|
33296
|
+
i += 2;
|
|
33297
|
+
} else if (annexB[i + 2] === 0 && annexB[i + 3] === 1) {
|
|
33298
|
+
starts.push({ idx: i, len: 4 });
|
|
33299
|
+
i += 3;
|
|
33300
|
+
}
|
|
33301
|
+
}
|
|
33302
|
+
}
|
|
33303
|
+
if (starts.length === 0) return [];
|
|
33304
|
+
const out = [];
|
|
33305
|
+
for (let s = 0; s < starts.length; s++) {
|
|
33306
|
+
const start = starts[s];
|
|
33307
|
+
const payloadStart = start.idx + start.len;
|
|
32452
33308
|
const next = starts[s + 1];
|
|
32453
33309
|
const payloadEnd = next ? next.idx : annexB.length;
|
|
32454
33310
|
if (payloadEnd > payloadStart) out.push(annexB.subarray(payloadStart, payloadEnd));
|
|
@@ -32469,7 +33325,7 @@ function isH264KeyframeFromAnnexB(annexB) {
|
|
|
32469
33325
|
}
|
|
32470
33326
|
return false;
|
|
32471
33327
|
}
|
|
32472
|
-
var BaichuanHttpStreamServer = class extends
|
|
33328
|
+
var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
|
|
32473
33329
|
videoStream;
|
|
32474
33330
|
listenPort;
|
|
32475
33331
|
path;
|
|
@@ -32533,7 +33389,7 @@ var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
|
|
|
32533
33389
|
this.httpServer.on("error", reject);
|
|
32534
33390
|
});
|
|
32535
33391
|
this.logger.info(`[BaichuanHttpStreamServer] Starting ffmpeg for H.264 -> MPEG-TS conversion...`);
|
|
32536
|
-
const ffmpeg = (0,
|
|
33392
|
+
const ffmpeg = (0, import_node_child_process9.spawn)("ffmpeg", [
|
|
32537
33393
|
"-hide_banner",
|
|
32538
33394
|
// ffmpeg warnings often include non-fatal decode messages (e.g. decode_slice_header),
|
|
32539
33395
|
// which we don't want to treat as application errors.
|
|
@@ -32736,15 +33592,15 @@ var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
|
|
|
32736
33592
|
};
|
|
32737
33593
|
|
|
32738
33594
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
32739
|
-
var
|
|
33595
|
+
var import_node_events9 = require("events");
|
|
32740
33596
|
var http5 = __toESM(require("http"), 1);
|
|
32741
33597
|
|
|
32742
33598
|
// src/baichuan/stream/MjpegTransformer.ts
|
|
32743
|
-
var
|
|
32744
|
-
var
|
|
33599
|
+
var import_node_events8 = require("events");
|
|
33600
|
+
var import_node_child_process10 = require("child_process");
|
|
32745
33601
|
var JPEG_SOI = Buffer.from([255, 216]);
|
|
32746
33602
|
var JPEG_EOI = Buffer.from([255, 217]);
|
|
32747
|
-
var MjpegTransformer = class extends
|
|
33603
|
+
var MjpegTransformer = class extends import_node_events8.EventEmitter {
|
|
32748
33604
|
options;
|
|
32749
33605
|
ffmpeg = null;
|
|
32750
33606
|
started = false;
|
|
@@ -32802,7 +33658,7 @@ var MjpegTransformer = class extends import_node_events7.EventEmitter {
|
|
|
32802
33658
|
"pipe:1"
|
|
32803
33659
|
);
|
|
32804
33660
|
this.log("debug", `Starting FFmpeg with args: ${args.join(" ")}`);
|
|
32805
|
-
this.ffmpeg = (0,
|
|
33661
|
+
this.ffmpeg = (0, import_node_child_process10.spawn)("ffmpeg", args, {
|
|
32806
33662
|
stdio: ["pipe", "pipe", "pipe"]
|
|
32807
33663
|
});
|
|
32808
33664
|
this.ffmpeg.stdout.on("data", (data) => {
|
|
@@ -32943,7 +33799,7 @@ Content-Length: ${frame.length}\r
|
|
|
32943
33799
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
32944
33800
|
init_H264Converter();
|
|
32945
33801
|
init_H265Converter();
|
|
32946
|
-
var BaichuanMjpegServer = class extends
|
|
33802
|
+
var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
|
|
32947
33803
|
options;
|
|
32948
33804
|
clients = /* @__PURE__ */ new Map();
|
|
32949
33805
|
httpServer = null;
|
|
@@ -33224,7 +34080,7 @@ var BaichuanMjpegServer = class extends import_node_events8.EventEmitter {
|
|
|
33224
34080
|
};
|
|
33225
34081
|
|
|
33226
34082
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
33227
|
-
var
|
|
34083
|
+
var import_node_events10 = require("events");
|
|
33228
34084
|
init_BcMediaAnnexBDecoder();
|
|
33229
34085
|
init_H264Converter();
|
|
33230
34086
|
function parseAnnexBNalUnits(annexB) {
|
|
@@ -33261,7 +34117,7 @@ function getH264NalType(nalUnit) {
|
|
|
33261
34117
|
function getH265NalType2(nalUnit) {
|
|
33262
34118
|
return nalUnit[0] >> 1 & 63;
|
|
33263
34119
|
}
|
|
33264
|
-
var BaichuanWebRTCServer = class extends
|
|
34120
|
+
var BaichuanWebRTCServer = class extends import_node_events10.EventEmitter {
|
|
33265
34121
|
options;
|
|
33266
34122
|
sessions = /* @__PURE__ */ new Map();
|
|
33267
34123
|
sessionIdCounter = 0;
|
|
@@ -34163,12 +35019,12 @@ Error: ${err}`
|
|
|
34163
35019
|
};
|
|
34164
35020
|
|
|
34165
35021
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
34166
|
-
var
|
|
35022
|
+
var import_node_events11 = require("events");
|
|
34167
35023
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
34168
35024
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
34169
35025
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
34170
35026
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
34171
|
-
var
|
|
35027
|
+
var import_node_child_process11 = require("child_process");
|
|
34172
35028
|
init_BcMediaAnnexBDecoder();
|
|
34173
35029
|
init_H264Converter();
|
|
34174
35030
|
init_H265Converter();
|
|
@@ -34243,7 +35099,7 @@ function getNalTypes(codec, annexB) {
|
|
|
34243
35099
|
}
|
|
34244
35100
|
});
|
|
34245
35101
|
}
|
|
34246
|
-
var BaichuanHlsServer = class extends
|
|
35102
|
+
var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
|
|
34247
35103
|
api;
|
|
34248
35104
|
channel;
|
|
34249
35105
|
profile;
|
|
@@ -34638,7 +35494,7 @@ var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
|
|
|
34638
35494
|
this.segmentPattern,
|
|
34639
35495
|
this.playlistPath
|
|
34640
35496
|
);
|
|
34641
|
-
const p = (0,
|
|
35497
|
+
const p = (0, import_node_child_process11.spawn)(this.ffmpegPath, args, {
|
|
34642
35498
|
stdio: ["pipe", "ignore", "pipe"]
|
|
34643
35499
|
});
|
|
34644
35500
|
p.on("error", (err) => {
|
|
@@ -34736,8 +35592,8 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
|
|
|
34736
35592
|
async function pingHost(host, timeoutMs = 3e3) {
|
|
34737
35593
|
return new Promise((resolve) => {
|
|
34738
35594
|
const { exec } = require("child_process");
|
|
34739
|
-
const
|
|
34740
|
-
const pingCmd =
|
|
35595
|
+
const platform2 = process.platform;
|
|
35596
|
+
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
34741
35597
|
// macOS: -W is in milliseconds (Linux: seconds)
|
|
34742
35598
|
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
34743
35599
|
) : (
|
|
@@ -35244,10 +36100,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
35244
36100
|
}
|
|
35245
36101
|
|
|
35246
36102
|
// src/multifocal/compositeRtspServer.ts
|
|
35247
|
-
var
|
|
35248
|
-
var
|
|
35249
|
-
var
|
|
35250
|
-
var CompositeRtspServer = class extends
|
|
36103
|
+
var import_node_events12 = require("events");
|
|
36104
|
+
var import_node_child_process12 = require("child_process");
|
|
36105
|
+
var net5 = __toESM(require("net"), 1);
|
|
36106
|
+
var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
35251
36107
|
options;
|
|
35252
36108
|
compositeStream = null;
|
|
35253
36109
|
rtspServer = null;
|
|
@@ -35313,7 +36169,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
35313
36169
|
const width = widerStreamInfo?.width ?? 1920;
|
|
35314
36170
|
const height = widerStreamInfo?.height ?? 1080;
|
|
35315
36171
|
const fps = widerStreamInfo?.frameRate ?? 25;
|
|
35316
|
-
this.rtspServer =
|
|
36172
|
+
this.rtspServer = net5.createServer((socket) => {
|
|
35317
36173
|
this.handleRtspConnection(socket);
|
|
35318
36174
|
});
|
|
35319
36175
|
await new Promise((resolve, reject) => {
|
|
@@ -35352,7 +36208,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
35352
36208
|
this.logger.log?.(
|
|
35353
36209
|
`[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
|
|
35354
36210
|
);
|
|
35355
|
-
this.ffmpegProcess = (0,
|
|
36211
|
+
this.ffmpegProcess = (0, import_node_child_process12.spawn)("ffmpeg", ffmpegArgs, {
|
|
35356
36212
|
stdio: ["pipe", "pipe", "pipe"]
|
|
35357
36213
|
});
|
|
35358
36214
|
this.ffmpegProcess.on("error", (error) => {
|
|
@@ -35589,6 +36445,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
35589
36445
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
35590
36446
|
DUAL_LENS_MODELS,
|
|
35591
36447
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
36448
|
+
Go2rtcTcpServer,
|
|
35592
36449
|
H264RtpDepacketizer,
|
|
35593
36450
|
H265RtpDepacketizer,
|
|
35594
36451
|
HlsSessionManager,
|
|
@@ -35656,7 +36513,11 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
|
|
|
35656
36513
|
detectIosClient,
|
|
35657
36514
|
detectVideoCodecFromNal,
|
|
35658
36515
|
discoverReolinkDevices,
|
|
36516
|
+
discoverViaArpTable,
|
|
36517
|
+
discoverViaDhcpListener,
|
|
35659
36518
|
discoverViaHttpScan,
|
|
36519
|
+
discoverViaOnvif,
|
|
36520
|
+
discoverViaTcpPortScan,
|
|
35660
36521
|
discoverViaUdpBroadcast,
|
|
35661
36522
|
discoverViaUdpDirect,
|
|
35662
36523
|
encodeHeader,
|