@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.js CHANGED
@@ -22,7 +22,11 @@ import {
22
22
  createTaggedLogger,
23
23
  decodeHeader,
24
24
  discoverReolinkDevices,
25
+ discoverViaArpTable,
26
+ discoverViaDhcpListener,
25
27
  discoverViaHttpScan,
28
+ discoverViaOnvif,
29
+ discoverViaTcpPortScan,
26
30
  discoverViaUdpBroadcast,
27
31
  discoverViaUdpDirect,
28
32
  encodeHeader,
@@ -38,7 +42,7 @@ import {
38
42
  normalizeUid,
39
43
  parseSupportXml,
40
44
  setGlobalLogger
41
- } from "./chunk-EG5IY3CM.js";
45
+ } from "./chunk-UDS2UR4S.js";
42
46
  import {
43
47
  AesStreamDecryptor,
44
48
  BC_AES_IV,
@@ -586,51 +590,18 @@ var AutodiscoveryClient = class {
586
590
  scanTimer = null;
587
591
  isRunning = false;
588
592
  currentScanPromise = null;
589
- /**
590
- * Costruttore del client di autodiscovery.
591
- *
592
- * @param options - Opzioni di configurazione per il discovery
593
- */
594
593
  constructor(options = {}) {
595
594
  this.options = {
596
- scanIntervalMs: options.scanIntervalMs ?? 6e4,
597
- // Default: 60 secondi
595
+ ...options,
596
+ scanIntervalMs: options.scanIntervalMs ?? 12e4,
598
597
  autoStart: options.autoStart ?? false
599
598
  };
600
- if (options.networkCidr !== void 0) {
601
- this.options.networkCidr = options.networkCidr;
602
- }
603
- if (options.username !== void 0) {
604
- this.options.username = options.username;
605
- }
606
- if (options.password !== void 0) {
607
- this.options.password = options.password;
608
- }
609
- if (options.httpProbeTimeoutMs !== void 0) {
610
- this.options.httpProbeTimeoutMs = options.httpProbeTimeoutMs;
611
- }
612
- if (options.maxConcurrentProbes !== void 0) {
613
- this.options.maxConcurrentProbes = options.maxConcurrentProbes;
614
- }
615
- if (options.logger !== void 0) {
616
- this.options.logger = options.logger;
617
- }
618
- if (options.httpPorts !== void 0) {
619
- this.options.httpPorts = options.httpPorts;
620
- }
621
- if (options.discoveryMethod !== void 0) {
622
- this.options.discoveryMethod = options.discoveryMethod;
623
- }
624
- if (options.udpBroadcastTimeoutMs !== void 0) {
625
- this.options.udpBroadcastTimeoutMs = options.udpBroadcastTimeoutMs;
626
- }
627
599
  if (this.options.autoStart) {
628
600
  this.start();
629
601
  }
630
602
  }
631
603
  /**
632
- * Avvia il discovery continuato.
633
- * Se già in esecuzione, non fa nulla.
604
+ * Start continuous discovery. If already running, does nothing.
634
605
  */
635
606
  start() {
636
607
  if (this.isRunning) {
@@ -645,8 +616,7 @@ var AutodiscoveryClient = class {
645
616
  this.scheduleNextScan();
646
617
  }
647
618
  /**
648
- * Ferma il discovery continuato.
649
- * Se non è in esecuzione, non fa nulla.
619
+ * Stop continuous discovery. If not running, does nothing.
650
620
  */
651
621
  stop() {
652
622
  if (!this.isRunning) {
@@ -661,50 +631,41 @@ var AutodiscoveryClient = class {
661
631
  this.options.logger?.log?.("[Autodiscovery] Discovery stopped");
662
632
  }
663
633
  /**
664
- * Restituisce la lista corrente delle telecamere discoverate.
665
- *
666
- * @returns Array di dispositivi discoverati, ordinati per host
634
+ * Returns the current list of discovered devices, sorted by host IP.
667
635
  */
668
636
  getDiscoveredDevices() {
669
- return Array.from(this.discoveredDevices.values()).sort((a, b) => {
670
- return a.host.localeCompare(b.host);
671
- });
637
+ return Array.from(this.discoveredDevices.values()).sort(
638
+ (a, b) => a.host.localeCompare(b.host)
639
+ );
672
640
  }
673
641
  /**
674
- * Restituisce il numero di telecamere attualmente discoverate.
675
- *
676
- * @returns Numero di dispositivi discoverati
642
+ * Returns the number of currently discovered devices.
677
643
  */
678
644
  getDeviceCount() {
679
645
  return this.discoveredDevices.size;
680
646
  }
681
647
  /**
682
- * Verifica se il discovery è attualmente in esecuzione.
683
- *
684
- * @returns `true` se il discovery è in esecuzione, `false` altrimenti
648
+ * Returns whether continuous discovery is currently running.
685
649
  */
686
650
  isActive() {
687
651
  return this.isRunning;
688
652
  }
689
653
  /**
690
- * Forza un scan immediato (non aspetta l'intervallo programmato).
691
- * Se uno scan è già in corso, attende il completamento prima di avviarne uno nuovo.
692
- *
693
- * @returns Promise che si risolve quando lo scan è completato
654
+ * Force an immediate scan (doesn't wait for the scheduled interval).
655
+ * If a scan is already in progress, waits for it to complete.
694
656
  */
695
657
  async scanNow() {
696
658
  if (this.currentScanPromise) {
697
- this.options.logger?.log?.("[Autodiscovery] Scan already in progress, waiting for completion...");
659
+ this.options.logger?.log?.(
660
+ "[Autodiscovery] Scan already in progress, waiting for completion..."
661
+ );
698
662
  await this.currentScanPromise;
699
663
  return;
700
664
  }
701
665
  await this.performScan();
702
666
  }
703
667
  /**
704
- * Rimuove un dispositivo dalla lista (utile se si sa che non è più disponibile).
705
- *
706
- * @param host - Indirizzo IP del dispositivo da rimuovere
707
- * @returns `true` se il dispositivo è stato rimosso, `false` se non era presente
668
+ * Remove a device from the discovered list.
708
669
  */
709
670
  removeDevice(host) {
710
671
  const removed = this.discoveredDevices.delete(host);
@@ -714,59 +675,20 @@ var AutodiscoveryClient = class {
714
675
  return removed;
715
676
  }
716
677
  /**
717
- * Pulisce tutte le telecamere discoverate dalla lista.
678
+ * Clear all discovered devices.
718
679
  */
719
680
  clearDevices() {
720
681
  const count = this.discoveredDevices.size;
721
682
  this.discoveredDevices.clear();
722
- this.options.logger?.log?.(`[Autodiscovery] Removed ${count} device(s) from list`);
683
+ this.options.logger?.log?.(
684
+ `[Autodiscovery] Removed ${count} device(s) from list`
685
+ );
723
686
  }
724
- /**
725
- * Esegue un singolo scan della rete.
726
- */
727
687
  async performScan() {
728
688
  const scanPromise = (async () => {
729
689
  try {
730
690
  this.options.logger?.log?.("[Autodiscovery] Starting scan...");
731
- const discoveryMethod = this.options.discoveryMethod ?? "http";
732
- const discoveryOptions = {
733
- enableHttpScanning: discoveryMethod === "http" || discoveryMethod === "both",
734
- enableUdpDiscovery: discoveryMethod === "udp" || discoveryMethod === "both"
735
- };
736
- if (this.options.networkCidr !== void 0) {
737
- discoveryOptions.networkCidr = this.options.networkCidr;
738
- }
739
- if (this.options.username !== void 0) {
740
- discoveryOptions.username = this.options.username;
741
- }
742
- if (this.options.password !== void 0) {
743
- discoveryOptions.password = this.options.password;
744
- }
745
- if (this.options.httpProbeTimeoutMs !== void 0) {
746
- discoveryOptions.httpProbeTimeoutMs = this.options.httpProbeTimeoutMs;
747
- }
748
- if (this.options.maxConcurrentProbes !== void 0) {
749
- discoveryOptions.maxConcurrentProbes = this.options.maxConcurrentProbes;
750
- }
751
- if (this.options.logger !== void 0) {
752
- discoveryOptions.logger = this.options.logger;
753
- }
754
- if (this.options.httpPorts !== void 0) {
755
- discoveryOptions.httpPorts = this.options.httpPorts;
756
- }
757
- if (this.options.udpBroadcastTimeoutMs !== void 0) {
758
- discoveryOptions.udpBroadcastTimeoutMs = this.options.udpBroadcastTimeoutMs;
759
- }
760
- let discovered = [];
761
- if (discoveryMethod === "http" || discoveryMethod === "both") {
762
- const httpDevices = await discoverViaHttpScan(discoveryOptions);
763
- discovered.push(...httpDevices);
764
- }
765
- if (discoveryMethod === "udp" || discoveryMethod === "both") {
766
- const udpDevices = await discoverViaUdpBroadcast(discoveryOptions);
767
- discovered.push(...udpDevices);
768
- }
769
- const beforeCount = this.discoveredDevices.size;
691
+ const discovered = await discoverReolinkDevices(this.options);
770
692
  const newDevices = [];
771
693
  const updatedDevices = [];
772
694
  for (const device of discovered) {
@@ -781,38 +703,35 @@ var AutodiscoveryClient = class {
781
703
  newDevices.push(device);
782
704
  }
783
705
  }
784
- const afterCount = this.discoveredDevices.size;
785
706
  this.options.logger?.log?.(
786
- `[Autodiscovery] Scan completed: ${newDevices.length} new, ${updatedDevices.length} updated, total: ${afterCount}`
707
+ `[Autodiscovery] Scan completed: ${newDevices.length} new, ${updatedDevices.length} updated, total: ${this.discoveredDevices.size}`
787
708
  );
788
709
  for (const device of newDevices) {
789
- const details = [];
790
- if (device.model) details.push(`Model: ${device.model}`);
791
- if (device.name) details.push(`Name: ${device.name}`);
792
- if (device.uid) details.push(`UID: ${device.uid}`);
793
- if (device.firmwareVersion) details.push(`Firmware: ${device.firmwareVersion}`);
794
- if (device.httpPort) details.push(`HTTP Port: ${device.httpPort}`);
795
- if (device.httpsPort) details.push(`HTTPS Port: ${device.httpsPort}`);
796
- details.push(`Discovery Method: ${device.discoveryMethod}`);
797
- if (device.supportsHttps !== void 0) details.push(`HTTPS: ${device.supportsHttps}`);
798
- if (device.httpAccessible !== void 0) details.push(`HTTP Accessible: ${device.httpAccessible}`);
799
710
  this.options.logger?.log?.(
800
- `[Autodiscovery] \u{1F195} NEW DEVICE DISCOVERED - Host: ${device.host}${details.length > 0 ? ` | ${details.join(" | ")}` : ""}`
711
+ `[Autodiscovery] NEW DEVICE - ${device.host} | ${device.model ?? "unknown"} | ${device.name ?? ""} | via ${device.discoveryMethod}`
801
712
  );
713
+ try {
714
+ this.options.onDeviceDiscovered?.(device);
715
+ } catch {
716
+ }
717
+ }
718
+ for (const device of updatedDevices) {
719
+ try {
720
+ this.options.onDeviceUpdated?.(device);
721
+ } catch {
722
+ }
802
723
  }
803
724
  } catch (error) {
804
725
  const msg = error instanceof Error ? error.message : String(error);
805
- this.options.logger?.error?.(`[Autodiscovery] Error during scan: ${msg}`);
726
+ this.options.logger?.error?.(
727
+ `[Autodiscovery] Error during scan: ${msg}`
728
+ );
806
729
  }
807
730
  })();
808
731
  this.currentScanPromise = scanPromise;
809
732
  await scanPromise;
810
733
  this.currentScanPromise = null;
811
734
  }
812
- /**
813
- * Unisce le informazioni di un dispositivo esistente con quelle di un nuovo scan.
814
- * Restituisce il dispositivo aggiornato se ci sono state modifiche, altrimenti `null`.
815
- */
816
735
  mergeDeviceInfo(existing, updated) {
817
736
  let hasChanges = false;
818
737
  if (!existing.model && updated.model) {
@@ -849,13 +768,8 @@ var AutodiscoveryClient = class {
849
768
  }
850
769
  return hasChanges ? existing : null;
851
770
  }
852
- /**
853
- * Programma il prossimo scan.
854
- */
855
771
  scheduleNextScan() {
856
- if (!this.isRunning) {
857
- return;
858
- }
772
+ if (!this.isRunning) return;
859
773
  this.scanTimer = setTimeout(() => {
860
774
  this.scanTimer = null;
861
775
  if (this.isRunning) {
@@ -4671,8 +4585,527 @@ async function createReplayHttpServer(options) {
4671
4585
  };
4672
4586
  }
4673
4587
 
4674
- // src/baichuan/stream/BaichuanHttpStreamServer.ts
4588
+ // src/baichuan/stream/Go2rtcTcpServer.ts
4675
4589
  import { EventEmitter as EventEmitter2 } from "events";
4590
+ import * as net from "net";
4591
+ var AsyncBoundedQueue = class {
4592
+ maxItems;
4593
+ queue = [];
4594
+ waiting;
4595
+ closed = false;
4596
+ constructor(maxItems) {
4597
+ this.maxItems = Math.max(1, maxItems | 0);
4598
+ }
4599
+ push(item) {
4600
+ if (this.closed) return;
4601
+ if (this.waiting) {
4602
+ const { resolve } = this.waiting;
4603
+ this.waiting = void 0;
4604
+ resolve({ value: item, done: false });
4605
+ return;
4606
+ }
4607
+ this.queue.push(item);
4608
+ if (this.queue.length > this.maxItems) {
4609
+ this.queue.splice(0, this.queue.length - this.maxItems);
4610
+ }
4611
+ }
4612
+ close() {
4613
+ if (this.closed) return;
4614
+ this.closed = true;
4615
+ if (this.waiting) {
4616
+ const { resolve } = this.waiting;
4617
+ this.waiting = void 0;
4618
+ resolve({ value: void 0, done: true });
4619
+ }
4620
+ }
4621
+ async next() {
4622
+ if (this.closed) return { value: void 0, done: true };
4623
+ const item = this.queue.shift();
4624
+ if (item !== void 0) return { value: item, done: false };
4625
+ return await new Promise((resolve) => {
4626
+ this.waiting = { resolve };
4627
+ });
4628
+ }
4629
+ };
4630
+ var NativeStreamFanout = class {
4631
+ opts;
4632
+ queues = /* @__PURE__ */ new Map();
4633
+ source = null;
4634
+ running = false;
4635
+ pumpPromise = null;
4636
+ constructor(opts) {
4637
+ this.opts = opts;
4638
+ }
4639
+ start() {
4640
+ if (this.running) return;
4641
+ this.running = true;
4642
+ this.source = this.opts.createSource();
4643
+ this.pumpPromise = (async () => {
4644
+ try {
4645
+ for await (const frame of this.source) {
4646
+ try {
4647
+ this.opts.onFrame?.(frame);
4648
+ } catch {
4649
+ }
4650
+ for (const q of this.queues.values()) {
4651
+ q.push(frame);
4652
+ }
4653
+ }
4654
+ } catch (e) {
4655
+ this.opts.onError?.(e);
4656
+ } finally {
4657
+ for (const q of this.queues.values()) q.close();
4658
+ this.queues.clear();
4659
+ this.running = false;
4660
+ this.opts.onEnd?.();
4661
+ }
4662
+ })();
4663
+ }
4664
+ subscribe(id) {
4665
+ const q = new AsyncBoundedQueue(this.opts.maxQueueItems);
4666
+ this.queues.set(id, q);
4667
+ const self = this;
4668
+ return (async function* () {
4669
+ try {
4670
+ while (true) {
4671
+ const r = await q.next();
4672
+ if (r.done) return;
4673
+ yield r.value;
4674
+ }
4675
+ } finally {
4676
+ q.close();
4677
+ self.queues.delete(id);
4678
+ }
4679
+ })();
4680
+ }
4681
+ async stop() {
4682
+ if (!this.running) return;
4683
+ this.running = false;
4684
+ const src = this.source;
4685
+ this.source = null;
4686
+ for (const q of this.queues.values()) q.close();
4687
+ this.queues.clear();
4688
+ try {
4689
+ await src?.return(void 0);
4690
+ } catch {
4691
+ }
4692
+ try {
4693
+ await this.pumpPromise;
4694
+ } catch {
4695
+ }
4696
+ this.pumpPromise = null;
4697
+ }
4698
+ };
4699
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4700
+ api;
4701
+ channel;
4702
+ profile;
4703
+ variant;
4704
+ listenHost;
4705
+ listenPort;
4706
+ logger;
4707
+ deviceId;
4708
+ gracePeriodMs;
4709
+ prebufferMaxMs;
4710
+ maxBufferBytes;
4711
+ prestartStream;
4712
+ active = false;
4713
+ server;
4714
+ resolvedPort;
4715
+ // Native stream
4716
+ nativeFanout = null;
4717
+ nativeStreamActive = false;
4718
+ dedicatedSessionRelease;
4719
+ detectedVideoType;
4720
+ // Client tracking
4721
+ connectedClients = /* @__PURE__ */ new Set();
4722
+ clientSockets = /* @__PURE__ */ new Map();
4723
+ stopGraceTimer;
4724
+ // Prebuffer
4725
+ prebuffer = [];
4726
+ constructor(options) {
4727
+ super();
4728
+ this.api = options.api;
4729
+ this.channel = options.channel;
4730
+ this.profile = options.profile;
4731
+ this.variant = options.variant ?? "default";
4732
+ this.listenHost = options.listenHost ?? "127.0.0.1";
4733
+ this.listenPort = options.listenPort ?? 0;
4734
+ this.logger = options.logger ?? console;
4735
+ this.deviceId = options.deviceId;
4736
+ this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
4737
+ this.prebufferMaxMs = options.prebufferMs ?? 3e3;
4738
+ this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
4739
+ this.prestartStream = options.prestartStream ?? true;
4740
+ }
4741
+ // -----------------------------------------------------------------------
4742
+ // Public API
4743
+ // -----------------------------------------------------------------------
4744
+ /** Start listening. Resolves once the TCP server is bound. */
4745
+ async start() {
4746
+ if (this.active) return;
4747
+ this.active = true;
4748
+ this.server = net.createServer((socket) => this.handleClient(socket));
4749
+ this.server.on("error", (err) => {
4750
+ this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
4751
+ this.emit("error", err);
4752
+ });
4753
+ await new Promise((resolve, reject) => {
4754
+ this.server.listen(this.listenPort, this.listenHost, () => {
4755
+ const addr = this.server.address();
4756
+ this.resolvedPort = addr.port;
4757
+ this.logger.info?.(
4758
+ `[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
4759
+ );
4760
+ this.emit("listening", { host: addr.address, port: addr.port });
4761
+ resolve();
4762
+ });
4763
+ this.server.once("error", reject);
4764
+ });
4765
+ if (this.prestartStream) {
4766
+ this.logger.info?.(
4767
+ `[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
4768
+ );
4769
+ this.startNativeStream();
4770
+ }
4771
+ }
4772
+ /** Stop the server and all active streams. */
4773
+ async stop() {
4774
+ if (!this.active) return;
4775
+ this.active = false;
4776
+ clearTimeout(this.stopGraceTimer);
4777
+ for (const [id, sock] of this.clientSockets) {
4778
+ sock.destroy();
4779
+ this.connectedClients.delete(id);
4780
+ }
4781
+ this.clientSockets.clear();
4782
+ await this.stopNativeStream();
4783
+ if (this.server) {
4784
+ await new Promise((resolve) => {
4785
+ this.server.close(() => resolve());
4786
+ });
4787
+ this.server = void 0;
4788
+ }
4789
+ this.prebuffer = [];
4790
+ this.resolvedPort = void 0;
4791
+ this.emit("close");
4792
+ }
4793
+ /** The actual port the server is listening on (available after start()). */
4794
+ get port() {
4795
+ return this.resolvedPort;
4796
+ }
4797
+ /** The go2rtc-compatible source URL. */
4798
+ get go2rtcSourceUrl() {
4799
+ if (this.resolvedPort == null) return void 0;
4800
+ return `tcp://127.0.0.1:${this.resolvedPort}`;
4801
+ }
4802
+ /** Number of currently connected clients. */
4803
+ get clientCount() {
4804
+ return this.connectedClients.size;
4805
+ }
4806
+ // -----------------------------------------------------------------------
4807
+ // Client handling
4808
+ // -----------------------------------------------------------------------
4809
+ handleClient(socket) {
4810
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
4811
+ socket.setNoDelay(true);
4812
+ this.connectedClients.add(clientId);
4813
+ this.clientSockets.set(clientId, socket);
4814
+ this.logger.info?.(
4815
+ `[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
4816
+ );
4817
+ this.emit("client", clientId);
4818
+ if (this.stopGraceTimer) {
4819
+ clearTimeout(this.stopGraceTimer);
4820
+ this.stopGraceTimer = void 0;
4821
+ }
4822
+ if (!this.nativeStreamActive) {
4823
+ this.startNativeStream();
4824
+ }
4825
+ this.feedClient(clientId, socket).catch((err) => {
4826
+ this.logger.warn?.(
4827
+ `[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
4828
+ );
4829
+ });
4830
+ const cleanup = () => {
4831
+ this.removeClient(clientId);
4832
+ socket.destroy();
4833
+ };
4834
+ socket.on("error", cleanup);
4835
+ socket.on("close", cleanup);
4836
+ }
4837
+ async feedClient(clientId, socket) {
4838
+ const fanoutDeadline = Date.now() + 3e4;
4839
+ while (this.active && !this.nativeFanout) {
4840
+ if (socket.destroyed) return;
4841
+ if (Date.now() > fanoutDeadline) {
4842
+ this.logger.warn?.(
4843
+ `[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
4844
+ );
4845
+ return;
4846
+ }
4847
+ await new Promise((r) => setTimeout(r, 100));
4848
+ }
4849
+ if (!this.active || !this.nativeFanout) return;
4850
+ const subscription = this.nativeFanout.subscribe(clientId);
4851
+ const prebufferSnap = this.prebuffer.slice();
4852
+ let lastIdrIdx = -1;
4853
+ for (let i = prebufferSnap.length - 1; i >= 0; i--) {
4854
+ if (prebufferSnap[i].isKeyframe) {
4855
+ lastIdrIdx = i;
4856
+ break;
4857
+ }
4858
+ }
4859
+ if (lastIdrIdx >= 0) {
4860
+ const replay = prebufferSnap.slice(lastIdrIdx);
4861
+ this.logger.info?.(
4862
+ `[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
4863
+ );
4864
+ for (const entry of replay) {
4865
+ if (socket.destroyed) return;
4866
+ socket.write(entry.data);
4867
+ }
4868
+ }
4869
+ let seenKeyframe = lastIdrIdx >= 0;
4870
+ let liveFrameCount = 0;
4871
+ let liveVideoWritten = 0;
4872
+ let lastLogAt = Date.now();
4873
+ try {
4874
+ this.logger.info?.(
4875
+ `[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
4876
+ );
4877
+ for await (const frame of subscription) {
4878
+ if (socket.destroyed || !this.active) {
4879
+ this.logger.info?.(
4880
+ `[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
4881
+ );
4882
+ break;
4883
+ }
4884
+ liveFrameCount++;
4885
+ const annexB = this.convertFrame(frame);
4886
+ if (!annexB) continue;
4887
+ if (!seenKeyframe) {
4888
+ if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
4889
+ seenKeyframe = true;
4890
+ this.logger.info?.(
4891
+ `[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
4892
+ );
4893
+ }
4894
+ socket.write(annexB);
4895
+ liveVideoWritten++;
4896
+ if (Date.now() - lastLogAt > 1e4) {
4897
+ this.logger.info?.(
4898
+ `[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
4899
+ );
4900
+ lastLogAt = Date.now();
4901
+ }
4902
+ if (socket.writableLength > this.maxBufferBytes) {
4903
+ this.logger.warn?.(
4904
+ `[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
4905
+ );
4906
+ socket.destroy();
4907
+ break;
4908
+ }
4909
+ }
4910
+ this.logger.info?.(
4911
+ `[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
4912
+ );
4913
+ } finally {
4914
+ await subscription.return(void 0).catch(() => {
4915
+ });
4916
+ }
4917
+ }
4918
+ // -----------------------------------------------------------------------
4919
+ // Frame conversion
4920
+ // -----------------------------------------------------------------------
4921
+ /**
4922
+ * Convert a native frame to wire-ready Annex-B.
4923
+ * Audio frames are skipped — raw TCP carries only video (Annex-B).
4924
+ * go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
4925
+ */
4926
+ convertFrame(frame) {
4927
+ if (frame.audio) {
4928
+ return null;
4929
+ }
4930
+ if (frame.data.length === 0) return null;
4931
+ try {
4932
+ if (frame.videoType === "H264") {
4933
+ return convertToAnnexB(frame.data);
4934
+ }
4935
+ if (frame.videoType === "H265") {
4936
+ return convertToAnnexB2(frame.data);
4937
+ }
4938
+ } catch {
4939
+ }
4940
+ return frame.data;
4941
+ }
4942
+ /** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
4943
+ isAnnexBKeyframe(annexB, videoType) {
4944
+ try {
4945
+ if (videoType === "H264") {
4946
+ const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
4947
+ return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
4948
+ }
4949
+ if (videoType === "H265") {
4950
+ const nals = splitAnnexBToNalPayloads2(annexB);
4951
+ return nals.some(
4952
+ (n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
4953
+ );
4954
+ }
4955
+ } catch {
4956
+ }
4957
+ return false;
4958
+ }
4959
+ /** Split Annex-B byte stream into individual NAL units. */
4960
+ static splitAnnexBNals(buf) {
4961
+ const nals = [];
4962
+ let i = 0;
4963
+ while (i < buf.length) {
4964
+ if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
4965
+ let scLen;
4966
+ if (buf[i + 2] === 1) {
4967
+ scLen = 3;
4968
+ } else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
4969
+ scLen = 4;
4970
+ } else {
4971
+ i++;
4972
+ continue;
4973
+ }
4974
+ const nalStart = i + scLen;
4975
+ let nalEnd = buf.length;
4976
+ for (let j = nalStart; j < buf.length - 2; j++) {
4977
+ if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
4978
+ nalEnd = j;
4979
+ break;
4980
+ }
4981
+ }
4982
+ if (nalEnd > nalStart) {
4983
+ nals.push(buf.subarray(nalStart, nalEnd));
4984
+ }
4985
+ i = nalEnd;
4986
+ } else {
4987
+ i++;
4988
+ }
4989
+ }
4990
+ return nals;
4991
+ }
4992
+ // -----------------------------------------------------------------------
4993
+ // Native stream management
4994
+ // -----------------------------------------------------------------------
4995
+ async startNativeStream() {
4996
+ if (this.nativeStreamActive) return;
4997
+ this.nativeStreamActive = true;
4998
+ let dedicatedClient;
4999
+ if (this.deviceId) {
5000
+ try {
5001
+ const session = await this.api.createDedicatedSession(
5002
+ `live:${this.deviceId}:ch${this.channel}:${this.profile}`
5003
+ );
5004
+ dedicatedClient = session.client;
5005
+ this.dedicatedSessionRelease = session.release;
5006
+ } catch (e) {
5007
+ this.logger.warn?.(
5008
+ `[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
5009
+ );
5010
+ }
5011
+ }
5012
+ this.logger.info?.(
5013
+ `[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
5014
+ );
5015
+ this.nativeFanout = new NativeStreamFanout({
5016
+ maxQueueItems: 200,
5017
+ createSource: () => createNativeStream(this.api, this.channel, this.profile, {
5018
+ variant: this.variant,
5019
+ ...dedicatedClient ? { client: dedicatedClient } : {}
5020
+ }),
5021
+ onFrame: (frame) => {
5022
+ if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
5023
+ this.detectedVideoType = frame.videoType;
5024
+ }
5025
+ const wireData = this.convertFrame(frame);
5026
+ if (!wireData || wireData.length === 0) return;
5027
+ const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
5028
+ this.prebuffer.push({
5029
+ data: Buffer.from(wireData),
5030
+ time: Date.now(),
5031
+ isKeyframe,
5032
+ audio: frame.audio
5033
+ });
5034
+ const cutoff = Date.now() - this.prebufferMaxMs;
5035
+ let trimIdx = 0;
5036
+ while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
5037
+ trimIdx++;
5038
+ }
5039
+ if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
5040
+ },
5041
+ onError: (error) => {
5042
+ this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
5043
+ },
5044
+ onEnd: () => {
5045
+ if (!this.nativeStreamActive) return;
5046
+ this.nativeStreamActive = false;
5047
+ this.nativeFanout = null;
5048
+ if (this.dedicatedSessionRelease) {
5049
+ this.dedicatedSessionRelease().catch(() => {
5050
+ });
5051
+ this.dedicatedSessionRelease = void 0;
5052
+ }
5053
+ if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
5054
+ this.logger.info?.(
5055
+ `[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
5056
+ );
5057
+ this.startNativeStream();
5058
+ }
5059
+ }
5060
+ });
5061
+ this.nativeFanout.start();
5062
+ }
5063
+ async stopNativeStream() {
5064
+ this.nativeStreamActive = false;
5065
+ const fanout = this.nativeFanout;
5066
+ this.nativeFanout = null;
5067
+ if (fanout) {
5068
+ await fanout.stop();
5069
+ }
5070
+ this.prebuffer = [];
5071
+ if (this.dedicatedSessionRelease) {
5072
+ await this.dedicatedSessionRelease().catch(() => {
5073
+ });
5074
+ this.dedicatedSessionRelease = void 0;
5075
+ }
5076
+ }
5077
+ // -----------------------------------------------------------------------
5078
+ // Client lifecycle
5079
+ // -----------------------------------------------------------------------
5080
+ removeClient(clientId) {
5081
+ if (!this.connectedClients.has(clientId)) return;
5082
+ this.connectedClients.delete(clientId);
5083
+ this.clientSockets.delete(clientId);
5084
+ this.logger.info?.(
5085
+ `[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
5086
+ );
5087
+ this.emit("clientDisconnected", clientId);
5088
+ if (this.connectedClients.size === 0 && !this.prestartStream) {
5089
+ this.scheduleStop();
5090
+ }
5091
+ }
5092
+ scheduleStop() {
5093
+ if (this.stopGraceTimer) return;
5094
+ this.logger.info?.(
5095
+ `[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
5096
+ );
5097
+ this.stopGraceTimer = setTimeout(async () => {
5098
+ this.stopGraceTimer = void 0;
5099
+ if (this.connectedClients.size === 0 && this.nativeStreamActive) {
5100
+ this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
5101
+ await this.stopNativeStream();
5102
+ }
5103
+ }, this.gracePeriodMs);
5104
+ }
5105
+ };
5106
+
5107
+ // src/baichuan/stream/BaichuanHttpStreamServer.ts
5108
+ import { EventEmitter as EventEmitter3 } from "events";
4676
5109
  import { spawn as spawn5 } from "child_process";
4677
5110
  import * as http4 from "http";
4678
5111
  var NAL_START_CODE_4B = Buffer.from([0, 0, 0, 1]);
@@ -4719,7 +5152,7 @@ function isH264KeyframeFromAnnexB(annexB) {
4719
5152
  }
4720
5153
  return false;
4721
5154
  }
4722
- var BaichuanHttpStreamServer = class extends EventEmitter2 {
5155
+ var BaichuanHttpStreamServer = class extends EventEmitter3 {
4723
5156
  videoStream;
4724
5157
  listenPort;
4725
5158
  path;
@@ -4986,15 +5419,15 @@ var BaichuanHttpStreamServer = class extends EventEmitter2 {
4986
5419
  };
4987
5420
 
4988
5421
  // src/baichuan/stream/BaichuanMjpegServer.ts
4989
- import { EventEmitter as EventEmitter4 } from "events";
5422
+ import { EventEmitter as EventEmitter5 } from "events";
4990
5423
  import * as http5 from "http";
4991
5424
 
4992
5425
  // src/baichuan/stream/MjpegTransformer.ts
4993
- import { EventEmitter as EventEmitter3 } from "events";
5426
+ import { EventEmitter as EventEmitter4 } from "events";
4994
5427
  import { spawn as spawn6 } from "child_process";
4995
5428
  var JPEG_SOI = Buffer.from([255, 216]);
4996
5429
  var JPEG_EOI = Buffer.from([255, 217]);
4997
- var MjpegTransformer = class extends EventEmitter3 {
5430
+ var MjpegTransformer = class extends EventEmitter4 {
4998
5431
  options;
4999
5432
  ffmpeg = null;
5000
5433
  started = false;
@@ -5191,7 +5624,7 @@ Content-Length: ${frame.length}\r
5191
5624
  }
5192
5625
 
5193
5626
  // src/baichuan/stream/BaichuanMjpegServer.ts
5194
- var BaichuanMjpegServer = class extends EventEmitter4 {
5627
+ var BaichuanMjpegServer = class extends EventEmitter5 {
5195
5628
  options;
5196
5629
  clients = /* @__PURE__ */ new Map();
5197
5630
  httpServer = null;
@@ -5472,7 +5905,7 @@ var BaichuanMjpegServer = class extends EventEmitter4 {
5472
5905
  };
5473
5906
 
5474
5907
  // src/baichuan/stream/BaichuanWebRTCServer.ts
5475
- import { EventEmitter as EventEmitter5 } from "events";
5908
+ import { EventEmitter as EventEmitter6 } from "events";
5476
5909
  function parseAnnexBNalUnits(annexB) {
5477
5910
  const nalUnits = [];
5478
5911
  let offset = 0;
@@ -5507,7 +5940,7 @@ function getH264NalType(nalUnit) {
5507
5940
  function getH265NalType2(nalUnit) {
5508
5941
  return nalUnit[0] >> 1 & 63;
5509
5942
  }
5510
- var BaichuanWebRTCServer = class extends EventEmitter5 {
5943
+ var BaichuanWebRTCServer = class extends EventEmitter6 {
5511
5944
  options;
5512
5945
  sessions = /* @__PURE__ */ new Map();
5513
5946
  sessionIdCounter = 0;
@@ -6409,7 +6842,7 @@ Error: ${err}`
6409
6842
  };
6410
6843
 
6411
6844
  // src/baichuan/stream/BaichuanHlsServer.ts
6412
- import { EventEmitter as EventEmitter6 } from "events";
6845
+ import { EventEmitter as EventEmitter7 } from "events";
6413
6846
  import fs from "fs";
6414
6847
  import fsp from "fs/promises";
6415
6848
  import os from "os";
@@ -6486,7 +6919,7 @@ function getNalTypes(codec, annexB) {
6486
6919
  }
6487
6920
  });
6488
6921
  }
6489
- var BaichuanHlsServer = class extends EventEmitter6 {
6922
+ var BaichuanHlsServer = class extends EventEmitter7 {
6490
6923
  api;
6491
6924
  channel;
6492
6925
  profile;
@@ -6904,10 +7337,10 @@ var BaichuanHlsServer = class extends EventEmitter6 {
6904
7337
  };
6905
7338
 
6906
7339
  // src/multifocal/compositeRtspServer.ts
6907
- import { EventEmitter as EventEmitter7 } from "events";
7340
+ import { EventEmitter as EventEmitter8 } from "events";
6908
7341
  import { spawn as spawn8 } from "child_process";
6909
- import * as net from "net";
6910
- var CompositeRtspServer = class extends EventEmitter7 {
7342
+ import * as net2 from "net";
7343
+ var CompositeRtspServer = class extends EventEmitter8 {
6911
7344
  options;
6912
7345
  compositeStream = null;
6913
7346
  rtspServer = null;
@@ -6973,7 +7406,7 @@ var CompositeRtspServer = class extends EventEmitter7 {
6973
7406
  const width = widerStreamInfo?.width ?? 1920;
6974
7407
  const height = widerStreamInfo?.height ?? 1080;
6975
7408
  const fps = widerStreamInfo?.frameRate ?? 25;
6976
- this.rtspServer = net.createServer((socket) => {
7409
+ this.rtspServer = net2.createServer((socket) => {
6977
7410
  this.handleRtspConnection(socket);
6978
7411
  });
6979
7412
  await new Promise((resolve, reject) => {
@@ -7248,6 +7681,7 @@ export {
7248
7681
  DUAL_LENS_DUAL_MOTION_MODELS,
7249
7682
  DUAL_LENS_MODELS,
7250
7683
  DUAL_LENS_SINGLE_MOTION_MODELS,
7684
+ Go2rtcTcpServer,
7251
7685
  H264RtpDepacketizer,
7252
7686
  H265RtpDepacketizer,
7253
7687
  HlsSessionManager,
@@ -7315,7 +7749,11 @@ export {
7315
7749
  detectIosClient,
7316
7750
  detectVideoCodecFromNal,
7317
7751
  discoverReolinkDevices,
7752
+ discoverViaArpTable,
7753
+ discoverViaDhcpListener,
7318
7754
  discoverViaHttpScan,
7755
+ discoverViaOnvif,
7756
+ discoverViaTcpPortScan,
7319
7757
  discoverViaUdpBroadcast,
7320
7758
  discoverViaUdpDirect,
7321
7759
  encodeHeader,