@apocaliss92/scrypted-reolink-native 0.3.5 → 0.3.7

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.
@@ -1,3 +1,258 @@
1
1
  /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
2
2
 
3
+ /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
4
+
5
+ /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
6
+
3
7
  /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
8
+
9
+ /**
10
+ * @preserve
11
+ * Copyright 2015-2018 Igor Bezkrovnyi
12
+ * All rights reserved. (MIT Licensed)
13
+ *
14
+ * cie94.ts - part of Image Quantization Library
15
+ */
16
+
17
+ /**
18
+ * @preserve
19
+ * Copyright 2015-2018 Igor Bezkrovnyi
20
+ * All rights reserved. (MIT Licensed)
21
+ *
22
+ * ciede2000.ts - part of Image Quantization Library
23
+ */
24
+
25
+ /**
26
+ * @preserve
27
+ * Copyright 2015-2018 Igor Bezkrovnyi
28
+ * All rights reserved. (MIT Licensed)
29
+ *
30
+ * cmetric.ts - part of Image Quantization Library
31
+ */
32
+
33
+ /**
34
+ * @preserve
35
+ * Copyright 2015-2018 Igor Bezkrovnyi
36
+ * All rights reserved. (MIT Licensed)
37
+ *
38
+ * common.ts - part of Image Quantization Library
39
+ */
40
+
41
+ /**
42
+ * @preserve
43
+ * Copyright 2015-2018 Igor Bezkrovnyi
44
+ * All rights reserved. (MIT Licensed)
45
+ *
46
+ * constants.ts - part of Image Quantization Library
47
+ */
48
+
49
+ /**
50
+ * @preserve
51
+ * Copyright 2015-2018 Igor Bezkrovnyi
52
+ * All rights reserved. (MIT Licensed)
53
+ *
54
+ * ditherErrorDiffusionArray.ts - part of Image Quantization Library
55
+ */
56
+
57
+ /**
58
+ * @preserve
59
+ * Copyright 2015-2018 Igor Bezkrovnyi
60
+ * All rights reserved. (MIT Licensed)
61
+ *
62
+ * euclidean.ts - part of Image Quantization Library
63
+ */
64
+
65
+ /**
66
+ * @preserve
67
+ * Copyright 2015-2018 Igor Bezkrovnyi
68
+ * All rights reserved. (MIT Licensed)
69
+ *
70
+ * helper.ts - part of Image Quantization Library
71
+ */
72
+
73
+ /**
74
+ * @preserve
75
+ * Copyright 2015-2018 Igor Bezkrovnyi
76
+ * All rights reserved. (MIT Licensed)
77
+ *
78
+ * hueStatistics.ts - part of Image Quantization Library
79
+ */
80
+
81
+ /**
82
+ * @preserve
83
+ * Copyright 2015-2018 Igor Bezkrovnyi
84
+ * All rights reserved. (MIT Licensed)
85
+ *
86
+ * iq.ts - Image Quantization Library
87
+ */
88
+
89
+ /**
90
+ * @preserve
91
+ * Copyright 2015-2018 Igor Bezkrovnyi
92
+ * All rights reserved. (MIT Licensed)
93
+ *
94
+ * lab2rgb.ts - part of Image Quantization Library
95
+ */
96
+
97
+ /**
98
+ * @preserve
99
+ * Copyright 2015-2018 Igor Bezkrovnyi
100
+ * All rights reserved. (MIT Licensed)
101
+ *
102
+ * lab2xyz.ts - part of Image Quantization Library
103
+ */
104
+
105
+ /**
106
+ * @preserve
107
+ * Copyright 2015-2018 Igor Bezkrovnyi
108
+ * All rights reserved. (MIT Licensed)
109
+ *
110
+ * manhattanNeuQuant.ts - part of Image Quantization Library
111
+ */
112
+
113
+ /**
114
+ * @preserve
115
+ * Copyright 2015-2018 Igor Bezkrovnyi
116
+ * All rights reserved. (MIT Licensed)
117
+ *
118
+ * nearestColor.ts - part of Image Quantization Library
119
+ */
120
+
121
+ /**
122
+ * @preserve
123
+ * Copyright 2015-2018 Igor Bezkrovnyi
124
+ * All rights reserved. (MIT Licensed)
125
+ *
126
+ * palette.ts - part of Image Quantization Library
127
+ */
128
+
129
+ /**
130
+ * @preserve
131
+ * Copyright 2015-2018 Igor Bezkrovnyi
132
+ * All rights reserved. (MIT Licensed)
133
+ *
134
+ * pngQuant.ts - part of Image Quantization Library
135
+ */
136
+
137
+ /**
138
+ * @preserve
139
+ * Copyright 2015-2018 Igor Bezkrovnyi
140
+ * All rights reserved. (MIT Licensed)
141
+ *
142
+ * point.ts - part of Image Quantization Library
143
+ */
144
+
145
+ /**
146
+ * @preserve
147
+ * Copyright 2015-2018 Igor Bezkrovnyi
148
+ * All rights reserved. (MIT Licensed)
149
+ *
150
+ * pointContainer.ts - part of Image Quantization Library
151
+ */
152
+
153
+ /**
154
+ * @preserve
155
+ * Copyright 2015-2018 Igor Bezkrovnyi
156
+ * All rights reserved. (MIT Licensed)
157
+ *
158
+ * rgb2hsl.ts - part of Image Quantization Library
159
+ */
160
+
161
+ /**
162
+ * @preserve
163
+ * Copyright 2015-2018 Igor Bezkrovnyi
164
+ * All rights reserved. (MIT Licensed)
165
+ *
166
+ * rgb2lab.ts - part of Image Quantization Library
167
+ */
168
+
169
+ /**
170
+ * @preserve
171
+ * Copyright 2015-2018 Igor Bezkrovnyi
172
+ * All rights reserved. (MIT Licensed)
173
+ *
174
+ * rgb2xyz.ts - part of Image Quantization Library
175
+ */
176
+
177
+ /**
178
+ * @preserve
179
+ * Copyright 2015-2018 Igor Bezkrovnyi
180
+ * All rights reserved. (MIT Licensed)
181
+ *
182
+ * ssim.ts - part of Image Quantization Library
183
+ */
184
+
185
+ /**
186
+ * @preserve
187
+ * Copyright 2015-2018 Igor Bezkrovnyi
188
+ * All rights reserved. (MIT Licensed)
189
+ *
190
+ * wuQuant.ts - part of Image Quantization Library
191
+ */
192
+
193
+ /**
194
+ * @preserve
195
+ * Copyright 2015-2018 Igor Bezkrovnyi
196
+ * All rights reserved. (MIT Licensed)
197
+ *
198
+ * xyz2lab.ts - part of Image Quantization Library
199
+ */
200
+
201
+ /**
202
+ * @preserve
203
+ * Copyright 2015-2018 Igor Bezkrovnyi
204
+ * All rights reserved. (MIT Licensed)
205
+ *
206
+ * xyz2rgb.ts - part of Image Quantization Library
207
+ */
208
+
209
+ /**
210
+ * @preserve
211
+ * MIT License
212
+ *
213
+ * Copyright 2015-2018 Igor Bezkrovnyi
214
+ *
215
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
216
+ * of this software and associated documentation files (the "Software"), to
217
+ * deal in the Software without restriction, including without limitation the
218
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
219
+ * sell copies of the Software, and to permit persons to whom the Software is
220
+ * furnished to do so, subject to the following conditions:
221
+ *
222
+ * The above copyright notice and this permission notice shall be included in
223
+ * all copies or substantial portions of the Software.
224
+ *
225
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
226
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
227
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
228
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
229
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
230
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
231
+ * IN THE SOFTWARE.
232
+ *
233
+ * riemersma.ts - part of Image Quantization Library
234
+ */
235
+
236
+ /**
237
+ * @preserve TypeScript port:
238
+ * Copyright 2015-2018 Igor Bezkrovnyi
239
+ * All rights reserved. (MIT Licensed)
240
+ *
241
+ * colorHistogram.ts - part of Image Quantization Library
242
+ */
243
+
244
+ /**
245
+ * @preserve TypeScript port:
246
+ * Copyright 2015-2018 Igor Bezkrovnyi
247
+ * All rights reserved. (MIT Licensed)
248
+ *
249
+ * neuquant.ts - part of Image Quantization Library
250
+ */
251
+
252
+ /**
253
+ * @preserve TypeScript port:
254
+ * Copyright 2015-2018 Igor Bezkrovnyi
255
+ * All rights reserved. (MIT Licensed)
256
+ *
257
+ * rgbquant.ts - part of Image Quantization Library
258
+ */
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
@@ -1,6 +1,7 @@
1
1
  import type { BaichuanClientOptions, ReolinkBaichuanApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import { ScryptedDeviceBase } from "@scrypted/sdk";
3
3
  import { createBaichuanApi, type BaichuanTransport } from "./connect";
4
+ import { StreamManager } from "./stream-utils";
4
5
 
5
6
  export interface BaichuanConnectionConfig {
6
7
  host: string;
@@ -176,6 +177,11 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
176
177
  private lastDisconnectTime: number = 0;
177
178
  private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
178
179
  private eventSubscriptionActive: boolean = false;
180
+ private lastEventTime: number = 0;
181
+ private pingInterval?: NodeJS.Timeout;
182
+ private autoRenewInterval?: NodeJS.Timeout;
183
+ private eventCheckInterval?: NodeJS.Timeout;
184
+ private consecutivePingFailures: number = 0;
179
185
 
180
186
  /**
181
187
  * Get the connection configuration for this instance
@@ -204,6 +210,12 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
204
210
  */
205
211
  protected abstract getStreamClientInputs(): BaichuanConnectionConfig;
206
212
 
213
+ /**
214
+ * Get StreamManager if available (optional, only for devices that support streaming)
215
+ * Override in subclasses that have a StreamManager
216
+ */
217
+ protected getStreamManager?(): StreamManager | undefined;
218
+
207
219
  /**
208
220
  * Get a Baichuan logger instance with formatting and debug control
209
221
  * This logger implements Console interface and can be used everywhere
@@ -293,6 +305,14 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
293
305
  this.baichuanApi = api;
294
306
  this.connectionTime = Date.now();
295
307
 
308
+ // Start ping and auto-renewal for TCP connections
309
+ if (this.transport === 'tcp') {
310
+ this.startConnectionMaintenance(api);
311
+ }
312
+
313
+ // Start event check for all connections
314
+ this.startEventCheck(api);
315
+
296
316
  return api;
297
317
  }
298
318
  catch (e) {
@@ -449,11 +469,206 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
449
469
  // ignore
450
470
  }
451
471
 
472
+ // Stop ping and auto-renewal intervals
473
+ this.stopConnectionMaintenance();
474
+
475
+ // Stop event check interval
476
+ this.stopEventCheck();
477
+
452
478
  // Reset state
453
479
  this.baichuanApi = undefined;
454
480
  this.ensureClientPromise = undefined;
455
481
  }
456
482
 
483
+ /**
484
+ * Get all active Baichuan connections (main + stream clients)
485
+ */
486
+ private getAllActiveConnections(): ReolinkBaichuanApi[] {
487
+ const connections: ReolinkBaichuanApi[] = [];
488
+
489
+ // Add main connection if exists and is valid
490
+ if (this.baichuanApi) {
491
+ const isConnected = this.baichuanApi.client.isSocketConnected();
492
+ const isLoggedIn = this.baichuanApi.client.loggedIn;
493
+ if (isConnected && isLoggedIn) {
494
+ connections.push(this.baichuanApi);
495
+ }
496
+ }
497
+
498
+ // Add all stream clients that are valid
499
+ for (const streamClient of this.streamClients.values()) {
500
+ const isConnected = streamClient.client.isSocketConnected();
501
+ const isLoggedIn = streamClient.client.loggedIn;
502
+ if (isConnected && isLoggedIn) {
503
+ connections.push(streamClient);
504
+ }
505
+ }
506
+
507
+ return connections;
508
+ }
509
+
510
+ /**
511
+ * Start ping and auto-renewal maintenance for TCP connections
512
+ */
513
+ private startConnectionMaintenance(api: ReolinkBaichuanApi): void {
514
+ const logger = this.getBaichuanLogger();
515
+
516
+ // Stop any existing intervals
517
+ this.stopConnectionMaintenance();
518
+
519
+ // Ping every 30 seconds to keep all connections alive
520
+ this.pingInterval = setInterval(async () => {
521
+ if (!this.baichuanApi || this.baichuanApi !== api) {
522
+ return; // Connection changed, stop this interval
523
+ }
524
+
525
+ try {
526
+ // Get all active connections (main + stream clients)
527
+ const allConnections = this.getAllActiveConnections();
528
+ logger.debug(`Pinging ${allConnections.length} connections`);
529
+
530
+ if (allConnections.length === 0) {
531
+ this.consecutivePingFailures++;
532
+ logger.debug(`No active connections found, failures=${this.consecutivePingFailures}`);
533
+
534
+ if (this.consecutivePingFailures >= 3) {
535
+ logger.log('No active connections detected, renewing connection');
536
+ await this.cleanupBaichuanApi();
537
+ this.consecutivePingFailures = 0;
538
+ }
539
+ return;
540
+ }
541
+
542
+ // Ping all connections using the specific ping method
543
+ const pingResults = await Promise.allSettled(
544
+ allConnections.map(async (conn) => {
545
+ try {
546
+ await conn.ping();
547
+ return { success: true, conn };
548
+ } catch (e) {
549
+ return { success: false, conn, error: e };
550
+ }
551
+ })
552
+ );
553
+
554
+ // Check results
555
+ const failedPings = pingResults.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.success));
556
+
557
+ if (failedPings.length > 0) {
558
+ this.consecutivePingFailures++;
559
+ logger.debug(`Ping failed for ${failedPings.length}/${allConnections.length} connections, failures=${this.consecutivePingFailures}`);
560
+
561
+ if (this.consecutivePingFailures >= 3) {
562
+ logger.log(`Multiple ping failures detected (${failedPings.length} connections), renewing connection`);
563
+ await this.cleanupBaichuanApi();
564
+ this.consecutivePingFailures = 0;
565
+ }
566
+ } else {
567
+ // All pings successful, reset failure counter
568
+ this.consecutivePingFailures = 0;
569
+ if (allConnections.length > 1) {
570
+ logger.debug(`Ping successful for all ${allConnections.length} connections`);
571
+ }
572
+ }
573
+ } catch (e) {
574
+ logger.debug(`Error in ping check: ${e?.message || String(e)}`);
575
+ }
576
+ }, 30_000); // Every 30 seconds
577
+
578
+ // Auto-renewal every 5 minutes if no active streams
579
+ this.autoRenewInterval = setInterval(async () => {
580
+ if (!this.baichuanApi || this.baichuanApi !== api) {
581
+ return; // Connection changed, stop this interval
582
+ }
583
+
584
+ try {
585
+ // Check if there are active streams
586
+ const hasActiveStreams = this.getStreamManager?.()?.hasActiveStreams() ?? false;
587
+
588
+ if (!hasActiveStreams) {
589
+ logger.log('No active streams detected, renewing connection (auto-renewal)');
590
+ await this.cleanupBaichuanApi();
591
+ } else {
592
+ logger.debug('Active streams detected, skipping auto-renewal');
593
+ }
594
+ } catch (e) {
595
+ logger.debug(`Error in auto-renewal check: ${e?.message || String(e)}`);
596
+ }
597
+ }, 5 * 60_000); // Every 5 minutes
598
+ }
599
+
600
+ /**
601
+ * Stop ping and auto-renewal maintenance
602
+ */
603
+ private stopConnectionMaintenance(): void {
604
+ if (this.pingInterval) {
605
+ clearInterval(this.pingInterval);
606
+ this.pingInterval = undefined;
607
+ }
608
+ if (this.autoRenewInterval) {
609
+ clearInterval(this.autoRenewInterval);
610
+ this.autoRenewInterval = undefined;
611
+ }
612
+ this.consecutivePingFailures = 0;
613
+ }
614
+
615
+ /**
616
+ * Start event check to monitor if events are being received
617
+ */
618
+ private startEventCheck(api: ReolinkBaichuanApi): void {
619
+ const logger = this.getBaichuanLogger();
620
+
621
+ // Stop any existing interval
622
+ this.stopEventCheck();
623
+
624
+ // Check every minute if events are being received
625
+ this.eventCheckInterval = setInterval(async () => {
626
+ if (!this.baichuanApi || this.baichuanApi !== api) {
627
+ return; // Connection changed, stop this interval
628
+ }
629
+
630
+ // Only check if event subscription is active
631
+ if (!this.eventSubscriptionActive) {
632
+ return;
633
+ }
634
+
635
+ try {
636
+ const now = Date.now();
637
+ const timeSinceLastEvent = now - this.lastEventTime;
638
+ const fiveMinutesMs = 5 * 60 * 1000;
639
+
640
+ if (this.lastEventTime > 0 && timeSinceLastEvent > fiveMinutesMs) {
641
+ logger.log(`No events received in the last ${Math.round(timeSinceLastEvent / 60_000)} minutes, restarting event listener`);
642
+ // Restart event subscription
643
+ await this.unsubscribeFromEvents();
644
+ await this.subscribeToEvents();
645
+ } else if (this.lastEventTime === 0) {
646
+ // If lastEventTime is 0, it means we just subscribed but haven't received any events yet
647
+ // Wait a bit longer before considering it a problem
648
+ const timeSinceSubscription = now - (this.connectionTime || now);
649
+ if (timeSinceSubscription > fiveMinutesMs) {
650
+ logger.log(`No events received since subscription (${Math.round(timeSinceSubscription / 60_000)} minutes ago), restarting event listener`);
651
+ await this.unsubscribeFromEvents();
652
+ await this.subscribeToEvents();
653
+ }
654
+ }
655
+ } catch (e) {
656
+ logger.debug(`Error in event check: ${e?.message || String(e)}`);
657
+ }
658
+ }, 5_000); // Check every minute
659
+ }
660
+
661
+ /**
662
+ * Stop event check interval
663
+ */
664
+ private stopEventCheck(): void {
665
+ if (this.eventCheckInterval) {
666
+ clearInterval(this.eventCheckInterval);
667
+ this.eventCheckInterval = undefined;
668
+ }
669
+ this.lastEventTime = 0;
670
+ }
671
+
457
672
  /**
458
673
  * Subscribe to Baichuan simple events
459
674
  */
@@ -493,10 +708,19 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
493
708
  return;
494
709
  }
495
710
 
496
- // Subscribe to events
711
+ // Subscribe to events with wrapper to track last event time
497
712
  try {
498
- await api.onSimpleEvent(callbacks.onSimpleEvent);
713
+ const originalHandler = callbacks.onSimpleEvent;
714
+ const wrappedHandler = (ev: ReolinkSimpleEvent) => {
715
+ // Update last event time
716
+ this.lastEventTime = Date.now();
717
+ // Call original handler
718
+ originalHandler(ev);
719
+ };
720
+
721
+ await api.onSimpleEvent(wrappedHandler);
499
722
  this.eventSubscriptionActive = true;
723
+ this.lastEventTime = Date.now(); // Initialize on subscription
500
724
  logger.log('Subscribed to Baichuan events');
501
725
  }
502
726
  catch (e) {
package/src/camera.ts CHANGED
@@ -1512,6 +1512,13 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1512
1512
  return convertDebugLogsToApiOptions(socketDebugLogs);
1513
1513
  }
1514
1514
 
1515
+ /**
1516
+ * Get StreamManager if available
1517
+ */
1518
+ protected getStreamManager(): StreamManager | undefined {
1519
+ return this.streamManager;
1520
+ }
1521
+
1515
1522
  /**
1516
1523
  * Initialize or recreate the StreamManager, taking into account multifocal composite options.
1517
1524
  */
@@ -2070,7 +2077,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2070
2077
  async takePictureInternal(client: ReolinkBaichuanApi) {
2071
2078
  const { rtspChannel, variantType } = this.storageSettings.values;
2072
2079
  const logger = this.getBaichuanLogger();
2073
- logger.log(`Taking new snapshot from camera: forceNewSnapshot=${this.forceNewSnapshot} channel=${rtspChannel} variant=${variantType}`);
2080
+ logger.debug(`Taking new snapshot from camera: forceNewSnapshot=${this.forceNewSnapshot} channel=${rtspChannel} variant=${variantType}`);
2074
2081
 
2075
2082
  const compositeOptions = this.isMultiFocal ? {
2076
2083
  widerChannel: this.isOnNvr ? rtspChannel : undefined,
@@ -2124,7 +2131,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2124
2131
  }
2125
2132
 
2126
2133
  if (!shouldTakeNewSnapshot && this.lastPicture) {
2127
- logger.log(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
2134
+ logger.debug(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
2128
2135
  return this.lastPicture.mo;
2129
2136
  }
2130
2137
 
@@ -2781,15 +2788,18 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2781
2788
 
2782
2789
  if (sleepStatus.state === 'sleeping') {
2783
2790
  if (!this.sleeping) {
2784
- this.getBaichuanLogger().log(`Camera is sleeping: ${sleepStatus.reason}`);
2791
+ this.getBaichuanLogger().log(`Camera is sleeping`);
2785
2792
  this.sleeping = true;
2786
2793
  }
2787
2794
  } else if (sleepStatus.state === 'awake') {
2788
2795
  // Camera is awake
2789
2796
  const wasSleeping = this.sleeping;
2790
2797
  if (wasSleeping) {
2791
- this.getBaichuanLogger().log(`Camera woke up: ${sleepStatus.reason}`);
2798
+ this.getBaichuanLogger().log(`Camera is awake`);
2792
2799
  this.sleeping = false;
2800
+ const client = await this.ensureClient();
2801
+ await client.wakeUp();
2802
+ await this.takePictureInternal(client);
2793
2803
  }
2794
2804
 
2795
2805
  if (wasSleeping) {
@@ -456,6 +456,18 @@ export class StreamManager {
456
456
  });
457
457
  }
458
458
 
459
+ /**
460
+ * Check if there are any active streams (servers that are listening).
461
+ */
462
+ hasActiveStreams(): boolean {
463
+ for (const server of this.nativeRfcServers.values()) {
464
+ if (server?.server?.listening) {
465
+ return true;
466
+ }
467
+ }
468
+ return false;
469
+ }
470
+
459
471
  /**
460
472
  * Close all active stream servers.
461
473
  * Useful when the main connection is reset and streams need to be recreated.