@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/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 === msgNum && frame.header.responseCode >= 400) {
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
- response += `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
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
- sendResponse(200, "OK", {
13600
- Session: sessionId,
13601
- Range: "npt=0.000-"
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
- this.prebuffer = [];
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: randomUUID2 } = await import("crypto");
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 = randomUUID2();
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
- scanIntervalMs: options.scanIntervalMs ?? 6e4,
28336
- // Default: 60 secondi
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
- * Avvia il discovery continuato.
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
- * Ferma il discovery continuato.
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
- * Restituisce la lista corrente delle telecamere discoverate.
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((a, b) => {
28409
- return a.host.localeCompare(b.host);
28410
- });
28797
+ return Array.from(this.discoveredDevices.values()).sort(
28798
+ (a, b) => a.host.localeCompare(b.host)
28799
+ );
28411
28800
  }
28412
28801
  /**
28413
- * Restituisce il numero di telecamere attualmente discoverate.
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
- * Verifica se il discovery è attualmente in esecuzione.
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
- * Forza un scan immediato (non aspetta l'intervallo programmato).
28430
- * Se uno scan è già in corso, attende il completamento prima di avviarne uno nuovo.
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?.("[Autodiscovery] Scan already in progress, waiting for completion...");
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
- * Rimuove un dispositivo dalla lista (utile se si sa che non è più disponibile).
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
- * Pulisce tutte le telecamere discoverate dalla lista.
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?.(`[Autodiscovery] Removed ${count} device(s) from list`);
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 discoveryMethod = this.options.discoveryMethod ?? "http";
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: ${afterCount}`
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] \u{1F195} NEW DEVICE DISCOVERED - Host: ${device.host}${details.length > 0 ? ` | ${details.join(" | ")}` : ""}`
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?.(`[Autodiscovery] Error during scan: ${msg}`);
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 platform = (clientInfo.secChUaPlatform ?? "").toLowerCase().replace(/"/g, "");
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") || platform === "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") || platform === "macos";
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 import_node_child_process4 = require("child_process");
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, import_node_child_process4.spawn)("ffmpeg", [
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, import_node_child_process4.spawn)("ffmpeg", [
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 import_node_child_process5 = require("child_process");
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, import_node_child_process5.spawn)("ffmpeg", [
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 import_node_crypto3 = __toESM(require("crypto"), 1);
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 = import_node_crypto3.default.randomBytes(2).readUInt16BE(0);
29417
- this.timestamp = import_node_crypto3.default.randomBytes(4).readUInt32BE(0);
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 = import_node_crypto3.default.randomBytes(4).readUInt32BE(0);
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 import_node_child_process6 = require("child_process");
29986
- var import_node_crypto4 = require("crypto");
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, import_node_crypto4.createHash)("sha1").update(slice).digest("hex");
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, import_node_child_process6.spawn)("ffmpeg", ffmpegArgs, {
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, import_node_child_process6.spawn)("ffmpeg", ffmpegArgs, {
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 import_node_child_process7 = require("child_process");
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, import_node_child_process7.spawn)(ffmpegPath, ffmpegArgs, {
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/BaichuanHttpStreamServer.ts
32759
+ // src/baichuan/stream/Go2rtcTcpServer.ts
32425
32760
  var import_node_events6 = require("events");
32426
- var import_node_child_process8 = require("child_process");
32427
- var http4 = __toESM(require("http"), 1);
32428
- var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
32429
- var NAL_START_CODE_3B3 = Buffer.from([0, 0, 1]);
32430
- function hasAnnexBStart(data) {
32431
- if (data.length < 4) return false;
32432
- return data.subarray(0, 4).equals(NAL_START_CODE_4B4) || data.subarray(0, 3).equals(NAL_START_CODE_3B3);
32433
- }
32434
- function splitAnnexBNals(annexB) {
32435
- const starts = [];
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
- if (starts.length === 0) return [];
32448
- const out = [];
32449
- for (let s = 0; s < starts.length; s++) {
32450
- const start = starts[s];
32451
- const payloadStart = start.idx + start.len;
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 import_node_events6.EventEmitter {
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, import_node_child_process8.spawn)("ffmpeg", [
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 import_node_events8 = require("events");
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 import_node_events7 = require("events");
32744
- var import_node_child_process9 = require("child_process");
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 import_node_events7.EventEmitter {
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, import_node_child_process9.spawn)("ffmpeg", args, {
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 import_node_events8.EventEmitter {
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 import_node_events9 = require("events");
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 import_node_events9.EventEmitter {
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 import_node_events10 = require("events");
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 import_node_child_process10 = require("child_process");
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 import_node_events10.EventEmitter {
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, import_node_child_process10.spawn)(this.ffmpegPath, args, {
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 platform = process.platform;
34740
- const pingCmd = platform === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform === "darwin" ? (
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 import_node_events11 = require("events");
35248
- var import_node_child_process11 = require("child_process");
35249
- var net3 = __toESM(require("net"), 1);
35250
- var CompositeRtspServer = class extends import_node_events11.EventEmitter {
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 = net3.createServer((socket) => {
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, import_node_child_process11.spawn)("ffmpeg", ffmpegArgs, {
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,