@cappitolian/network-discovery 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,11 +6,12 @@ A Capacitor plugin for network service discovery using mDNS/Bonjour. Allows auto
6
6
 
7
7
  ## Features
8
8
 
9
- - **Advertise services** on the local network (server mode)
9
+ - **Publish services** on the local network (server mode)
10
10
  - **Discover services** automatically (client mode)
11
- - Pass custom data via TXT records (like IP addresses, ports, etc.)
12
- - Real-time service found/lost events
13
- - Works on **iOS** (Bonjour) and **Android** (NSD)
11
+ - Pass custom metadata via TXT records (like HTTP port, version, etc.)
12
+ - Anti-ghosting IP resolution logic
13
+ - Works on **iOS** (Bonjour/NWListener) and **Android** (NSD)
14
+ - Cross-platform compatible: iOS ↔ Android in both directions
14
15
  - Tested with **Capacitor 7** and **Ionic 8**
15
16
 
16
17
  ---
@@ -30,133 +31,48 @@ npx cap sync
30
31
  import { NetworkDiscovery } from '@cappitolian/network-discovery';
31
32
  ```
32
33
 
33
- ### Server Mode - Advertise Your Service
34
+ ### Server Mode - Publish Your Service
34
35
  ```typescript
35
- // Start advertising your server
36
- await NetworkDiscovery.startAdvertising({
37
- serviceName: 'MyAppServer',
38
- serviceType: '_http._tcp', // or '_myapp._tcp' for custom service to avoid conflicts with other services
39
- port: 8080,
40
- txtRecord: {
41
- ip: '192.168.1.100', // Your server IP
42
- version: '1.0.0' // Any custom data
36
+ // Start publishing your server
37
+ await NetworkDiscovery.startServer({
38
+ serviceName: 'SSSPOSServer',
39
+ serviceType: '_ssspos._tcp',
40
+ port: 8081, // Discovery service port
41
+ ip: '192.168.1.100', // Your server IP
42
+ metadata: {
43
+ httpPort: '8080', // Your actual HTTP server port
44
+ version: '1.0.0', // Any custom data
45
+ deviceName: 'Main Server'
43
46
  }
44
47
  });
45
48
 
46
49
  console.log('Server is now discoverable on the network');
47
50
 
48
- // Stop advertising when needed
49
- await NetworkDiscovery.stopAdvertising();
51
+ // Stop publishing when needed
52
+ await NetworkDiscovery.stopServer();
50
53
  ```
51
54
 
52
55
  ### Client Mode - Discover Services
53
56
  ```typescript
54
- // Listen for discovered services
55
- NetworkDiscovery.addListener('serviceFound', (service) => {
56
- console.log('Service discovered:', service);
57
- console.log('Server IP:', service.txtRecord?.ip);
58
- console.log('Server Port:', service.port);
59
- console.log('Server Addresses:', service.addresses);
60
-
61
- // Connect to your server
62
- connectToServer(service.addresses[0], service.port);
63
- });
64
-
65
- // Listen for lost services
66
- NetworkDiscovery.addListener('serviceLost', (service) => {
67
- console.log('Service lost:', service.serviceName);
57
+ // Find a server on the network
58
+ const result = await NetworkDiscovery.findServer({
59
+ serviceName: 'SSSPOSServer',
60
+ serviceType: '_ssspos._tcp',
61
+ timeout: 15000 // Optional, defaults to 10000ms
68
62
  });
69
63
 
70
- // Start discovery
71
- await NetworkDiscovery.startDiscovery({
72
- serviceType: '_http._tcp', // Must match the server's serviceType or '_myapp._tcp' for custom service to avoid conflicts with other services
73
- domain: 'local.' // Optional, defaults to 'local.'
74
- });
75
-
76
- // Stop discovery when needed
77
- await NetworkDiscovery.stopDiscovery();
78
-
79
- // Clean up listeners
80
- await NetworkDiscovery.removeAllListeners();
81
- ```
82
-
83
- ### Complete Example - Auto-Connect Flow
84
-
85
- **Server Side:**
86
- ```typescript
87
- import { NetworkDiscovery } from '@cappitolian/network-discovery';
88
- import { Network } from '@capacitor/network';
89
-
90
- async startServer() {
91
- // Get device IP
92
- const ip = await this.getLocalIP();
93
-
94
- // Advertise server
95
- await NetworkDiscovery.startAdvertising({
96
- serviceName: 'MyAppServer',
97
- serviceType: '_http._tcp', // Must match the server's serviceType or '_myapp._tcp' for custom service to avoid conflicts with other services
98
- port: 8080,
99
- txtRecord: {
100
- ip: ip,
101
- serverName: 'Production Server'
102
- }
103
- });
64
+ if (result) {
65
+ console.log('Server found!');
66
+ console.log('IP:', result.ip);
67
+ console.log('Discovery Port:', result.port);
68
+ console.log('HTTP Port:', result.metadata.httpPort);
69
+ console.log('Version:', result.metadata.version);
104
70
 
105
- console.log('Server advertising on network');
106
- }
107
-
108
- async getLocalIP(): Promise<string> {
109
- const status = await Network.getStatus();
110
- // Your IP extraction logic here
111
- return '192.168.1.100';
112
- }
113
- ```
114
-
115
- **Client Side:**
116
- ```typescript
117
- import { NetworkDiscovery } from '@cappitolian/network-discovery';
118
-
119
- async findAndConnectToServer() {
120
- return new Promise((resolve, reject) => {
121
-
122
- // Listen for server
123
- NetworkDiscovery.addListener('serviceFound', async (service) => {
124
- if (service.serviceName === 'MyAppServer') {
125
- console.log('Server found!');
126
-
127
- const serverIP = service.txtRecord?.ip || service.addresses[0];
128
- const serverPort = service.port;
129
-
130
- // Stop discovery
131
- await NetworkDiscovery.stopDiscovery();
132
- await NetworkDiscovery.removeAllListeners();
133
-
134
- // Connect to server
135
- resolve({ ip: serverIP, port: serverPort });
136
- }
137
- });
138
-
139
- // Start discovery
140
- NetworkDiscovery.startDiscovery({
141
- serviceType: '_http._tcp' // Must match the server's serviceType or '_myapp._tcp' for custom service to avoid conflicts with other services
142
- });
143
-
144
- // Timeout after 10 seconds
145
- setTimeout(() => {
146
- reject(new Error('Server not found'));
147
- }, 10000);
148
- });
149
- }
150
-
151
- // Usage
152
- async onLoginClick() {
153
- try {
154
- const server = await this.findAndConnectToServer();
155
- console.log(`Connecting to ${server.ip}:${server.port}`);
156
- // Your connection logic here
157
- } catch (error) {
158
- console.error('Could not find server:', error);
159
- }
71
+ // Connect to your HTTP server
72
+ const httpUrl = `http://${result.ip}:${result.metadata.httpPort}`;
73
+ // Make your API calls here
74
+ } else {
75
+ console.log('Server not found within timeout');
160
76
  }
161
77
  ```
162
78
 
@@ -166,104 +82,70 @@ async onLoginClick() {
166
82
 
167
83
  ### Methods
168
84
 
169
- #### `startAdvertising(options: AdvertisingOptions)`
85
+ #### `startServer(options: StartServerOptions)`
170
86
 
171
87
  Publishes a service on the local network.
172
88
 
173
89
  **Parameters:**
174
- - `serviceName` (string): Name of your service (e.g., "MyAppServer")
175
- - `serviceType` (string): Service type (e.g., "_http._tcp", "_myapp._tcp")
176
- - `port` (number): Port number your server is listening on
177
- - `txtRecord` (object, optional): Key-value pairs to broadcast (e.g., IP, version)
178
-
179
- **Returns:** `Promise<{ success: boolean }>`
180
-
181
- ---
182
-
183
- #### `stopAdvertising()`
184
-
185
- Stops advertising the service.
90
+ ```typescript
91
+ {
92
+ serviceName: string; // Name of your service
93
+ serviceType: string; // Service type (e.g., "_ssspos._tcp")
94
+ port: number; // Discovery service port
95
+ ip: string; // Your server IP address
96
+ metadata?: { // Optional custom key-value pairs
97
+ [key: string]: string;
98
+ };
99
+ }
100
+ ```
186
101
 
187
- **Returns:** `Promise<{ success: boolean }>`
102
+ #### `stopServer()`
188
103
 
189
- ---
104
+ Stops publishing the service.
190
105
 
191
- #### `startDiscovery(options: DiscoveryOptions)`
106
+ #### `findServer(options: FindServerOptions)`
192
107
 
193
- Starts searching for services on the local network.
108
+ Searches for a service on the local network.
194
109
 
195
110
  **Parameters:**
196
- - `serviceType` (string): Type of service to search for (must match server's type)
197
- - `domain` (string, optional): Domain to search in (default: "local.")
198
-
199
- **Returns:** `Promise<void>`
200
-
201
- ---
202
-
203
- #### `stopDiscovery()`
204
-
205
- Stops the service discovery.
206
-
207
- **Returns:** `Promise<{ success: boolean }>`
208
-
209
- ---
210
-
211
- ### Events
212
-
213
- #### `serviceFound`
214
-
215
- Fired when a service is discovered.
216
-
217
- **Payload:**
218
111
  ```typescript
219
112
  {
220
113
  serviceName: string;
221
114
  serviceType: string;
222
- domain: string;
223
- hostName: string;
224
- port: number;
225
- addresses: string[]; // Array of IP addresses
226
- txtRecord?: { // Custom data from server
227
- [key: string]: string;
228
- };
115
+ timeout?: number; // Default: 10000ms
229
116
  }
230
117
  ```
231
118
 
232
- ---
233
-
234
- #### `serviceLost`
235
-
236
- Fired when a previously discovered service is no longer available.
237
-
238
- **Payload:**
119
+ **Returns:**
239
120
  ```typescript
240
121
  {
241
- serviceName: string;
242
- serviceType: string;
122
+ ip: string;
123
+ port: number;
124
+ metadata: { [key: string]: string };
243
125
  }
244
126
  ```
245
127
 
246
128
  ---
247
129
 
248
- ## Service Type Format
249
-
250
- Service types must follow the format: `_name._protocol`
130
+ ## Architecture Best Practices
251
131
 
252
- **Common examples:**
253
- - `_http._tcp` - HTTP service
254
- - `_https._tcp` - HTTPS service
255
- - `_myapp._tcp` - Custom app service
256
- - `_ssh._tcp` - SSH service
132
+ ### Separate Ports Pattern
133
+ ```
134
+ Port 8080 HTTP Server (GET, POST, API endpoints)
135
+ Port 8081 Network Discovery (mDNS/Bonjour)
136
+ ```
257
137
 
258
- **Recommended for apps:** Use a custom service type like `_myapp._tcp` to avoid conflicts with other services.
138
+ **Why:**
139
+ 1. Avoids port binding conflicts
140
+ 2. Clear separation of concerns
141
+ 3. Easier debugging
142
+ 4. HTTP server can restart without affecting discovery
259
143
 
260
144
  ---
261
145
 
262
146
  ## Permissions
263
147
 
264
148
  ### Android
265
-
266
- Add to `AndroidManifest.xml`:
267
149
  ```xml
268
150
  <uses-permission android:name="android.permission.INTERNET" />
269
151
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -272,33 +154,26 @@ Add to `AndroidManifest.xml`:
272
154
  ```
273
155
 
274
156
  ### iOS
275
-
276
- No additional permissions required. Bonjour is enabled by default.
277
-
278
- Optionally, add to `Info.plist` to declare your service:
279
157
  ```xml
158
+ <key>NSLocalNetworkUsageDescription</key>
159
+ <string>This app needs access to the local network to discover and connect to other devices.</string>
160
+
280
161
  <key>NSBonjourServices</key>
281
162
  <array>
282
- <string>_myapp._tcp</string>
163
+ <string>_ssspos._tcp</string>
283
164
  </array>
284
165
  ```
285
166
 
286
167
  ---
287
168
 
288
- ## Platforms
289
-
290
- - **iOS** (Bonjour/NetService)
291
- - **Android** (Network Service Discovery)
292
- - **Web** (Not implemented - throws unimplemented error)
293
-
294
- ---
295
-
296
- ## Requirements
169
+ ## Cross-Platform Compatibility
297
170
 
298
- - [Capacitor 7+](https://capacitorjs.com/)
299
- - [Ionic 8+](https://ionicframework.com/) (optional, but tested)
300
- - iOS 12.0+
301
- - Android API 16+ (Android 4.1+)
171
+ | Server → Client | Status |
172
+ |----------------|--------|
173
+ | Android → Android | ✅ Working |
174
+ | Android iOS | ✅ Working |
175
+ | iOS → Android | ✅ Working |
176
+ | iOS → iOS | ✅ Working |
302
177
 
303
178
  ---
304
179
 
@@ -306,39 +181,41 @@ Optionally, add to `Info.plist` to declare your service:
306
181
 
307
182
  ### Services not being discovered
308
183
 
309
- 1. **Check both devices are on the same WiFi network**
310
- 2. **Verify service types match** exactly between server and client
311
- 3. **Check firewall settings** - some networks block mDNS
312
- 4. **Android:** Ensure multicast is enabled on your network
313
- 5. **iOS:** Make sure device is not in Low Power Mode
184
+ 1. Check both devices are on same WiFi network
185
+ 2. Verify service types match exactly
186
+ 3. Check firewall/router settings (some block mDNS)
187
+ 4. Android: Ensure multicast is enabled
188
+ 5. iOS: Device not in Low Power Mode
314
189
 
315
- ### Server not advertising
190
+ ### iOS Server not visible to Android
316
191
 
317
- 1. **Verify port is not in use** by another service
318
- 2. **Check network permissions** are granted
319
- 3. **Restart the app** after installing the plugin
192
+ **Fixed in v1.0.0+**
320
193
 
321
- ### General debugging
194
+ If still experiencing issues:
195
+ 1. Update to latest version
196
+ 2. Use separate ports (8080 for HTTP, 8081 for discovery)
197
+ 3. Check logs for "iOS Servidor LISTO" (iOS) and "NSD_DEBUG" (Android)
322
198
 
323
- Enable verbose logging:
324
- ```typescript
325
- // Check plugin is loaded
326
- console.log('Plugin available:', NetworkDiscovery);
199
+ ### Debugging
327
200
 
328
- // Log all events
329
- NetworkDiscovery.addListener('serviceFound', (s) => console.log('Found:', s));
330
- NetworkDiscovery.addListener('serviceLost', (s) => console.log('Lost:', s));
201
+ **Android:**
202
+ ```bash
203
+ adb logcat | grep NSD_DEBUG
331
204
  ```
332
205
 
206
+ **iOS:**
207
+ Look for logs with `NSD_LOG:` in Xcode Console
208
+
333
209
  ---
334
210
 
335
- ## Use Cases
211
+ ## Changelog
336
212
 
337
- - **Auto-connect apps** - Client automatically finds and connects to server
338
- - **Local multiplayer games** - Discover game hosts on LAN
339
- - **IoT device discovery** - Find smart devices without configuration
340
- - **File sharing apps** - Discover peers for file transfer
341
- - **Remote control apps** - Find controllable devices automatically
213
+ ### v1.0.0 (Current)
214
+ - Fixed iOS server Android client discovery
215
+ - Added anti-ghosting IP resolution
216
+ - Improved cross-platform compatibility
217
+ - Enhanced logging for debugging
218
+ - ✅ Support for separate discovery/HTTP ports
342
219
 
343
220
  ---
344
221
 
@@ -346,20 +223,6 @@ NetworkDiscovery.addListener('serviceLost', (s) => console.log('Lost:', s));
346
223
 
347
224
  MIT
348
225
 
349
- ---
350
-
351
- ## Support
352
-
353
- If you encounter any issues or have feature requests, please open an issue on the [GitHub repository](https://github.com/alessandrycruz1987/network-discovery).
354
-
355
- ---
356
-
357
- ## Contributing
358
-
359
- Contributions are welcome! Please feel free to submit a Pull Request.
360
-
361
- ---
362
-
363
226
  ## Credits
364
227
 
365
- Created for use with Capacitor 7 and Ionic 8 applications requiring automatic network service discovery.
228
+ Developed by Alessandry Cruz for Cappitolian projects.
@@ -4,49 +4,155 @@ import android.content.Context;
4
4
  import android.net.nsd.NsdManager;
5
5
  import android.net.nsd.NsdServiceInfo;
6
6
  import android.net.wifi.WifiManager;
7
+ import android.os.Handler;
8
+ import android.os.Looper;
9
+ import com.getcapacitor.JSObject;
10
+ import java.nio.charset.StandardCharsets;
7
11
  import java.util.Map;
8
12
 
9
13
  public class NetworkDiscovery {
10
14
  private NsdManager nsdManager;
11
15
  private WifiManager.MulticastLock multicastLock;
12
- private NsdManager.RegistrationListener registrationListener;
16
+ private NsdManager.RegistrationListener activeRegistrationListener;
17
+ private NsdManager.DiscoveryListener activeDiscoveryListener;
18
+
19
+ public interface Callback {
20
+ void success(JSObject data);
21
+ void error(String msg);
22
+ }
13
23
 
14
24
  public NetworkDiscovery(Context context) {
15
25
  nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
16
26
  WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
17
27
  multicastLock = wm.createMulticastLock("SSSPOS_Lock");
28
+ multicastLock.setReferenceCounted(true);
18
29
  }
19
30
 
20
- public void startServer(String name, String type, int port, Map<String, String> metadata, final RegistrationCallback callback) {
21
- multicastLock.acquire(); // Asegura que el servicio sea visible
31
+ public void startServer(String name, String type, int port, Map<String, String> metadata, final Callback callback) {
32
+ stopServer(null);
33
+ if (!multicastLock.isHeld()) multicastLock.acquire();
34
+
35
+ // TRUCO: Incluimos la IP en el nombre por si los metadatos fallan en iOS
36
+ String ipForName = (metadata != null && metadata.containsKey("ip")) ? metadata.get("ip") : "";
37
+ String displayName = ipForName.isEmpty() ? name : name + "-" + ipForName;
22
38
 
23
39
  NsdServiceInfo serviceInfo = new NsdServiceInfo();
24
- serviceInfo.setServiceName(name);
40
+ serviceInfo.setServiceName(displayName);
25
41
  serviceInfo.setServiceType(type);
26
42
  serviceInfo.setPort(port);
27
43
 
44
+ // IMPORTANTE: Atributos antes de registrar
28
45
  if (metadata != null) {
29
46
  for (Map.Entry<String, String> entry : metadata.entrySet()) {
30
- serviceInfo.setAttribute(entry.getKey(), entry.getValue());
47
+ serviceInfo.setAttribute(entry.getKey().toLowerCase(), entry.getValue());
31
48
  }
32
49
  }
33
50
 
34
- registrationListener = new NsdManager.RegistrationListener() {
35
- @Override
36
- public void onServiceRegistered(NsdServiceInfo info) { callback.onSuccess(); }
37
- @Override
38
- public void onRegistrationFailed(NsdServiceInfo info, int err) { callback.onError(err); }
39
- @Override
40
- public void onServiceUnregistered(NsdServiceInfo info) {}
51
+ activeRegistrationListener = new NsdManager.RegistrationListener() {
52
+ @Override public void onServiceRegistered(NsdServiceInfo info) { callback.success(new JSObject()); }
53
+ @Override public void onRegistrationFailed(NsdServiceInfo info, int err) { callback.error("REG_ERR_" + err); }
54
+ @Override public void onServiceUnregistered(NsdServiceInfo info) {}
55
+ @Override public void onUnregistrationFailed(NsdServiceInfo info, int err) {}
56
+ };
57
+
58
+ nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, activeRegistrationListener);
59
+ }
60
+
61
+ public void findServer(String name, String type, int timeout, final Callback callback) {
62
+ stopDiscovery();
63
+ if (!multicastLock.isHeld()) multicastLock.acquire();
64
+
65
+ final String cleanType = type.startsWith("_") ? type : "_" + type;
66
+ final boolean[] finished = {false};
67
+
68
+ activeDiscoveryListener = new NsdManager.DiscoveryListener() {
69
+ @Override public void onDiscoveryStarted(String s) {}
70
+ @Override public void onStartDiscoveryFailed(String s, int i) { if(!finished[0]){ finished[0]=true; callback.error("START_FAIL"); } }
41
71
  @Override
42
- public void onUnregistrationFailed(NsdServiceInfo info, int err) {}
72
+ public void onServiceFound(NsdServiceInfo service) {
73
+ // 1. Log para ver qué está viendo Android realmente antes de filtrar
74
+ System.out.println("NSD_DEBUG: Servicio encontrado en red: " + service.getServiceName());
75
+
76
+ // 2. Filtro estricto: El nombre debe empezar con el prefijo exacto
77
+ if (service.getServiceName().startsWith(name)) {
78
+ nsdManager.resolveService(service, new NsdManager.ResolveListener() {
79
+ @Override public void onResolveFailed(NsdServiceInfo nsi, int i) {
80
+ System.out.println("NSD_DEBUG: Fallo al resolver servicio: " + i);
81
+ }
82
+
83
+ @Override
84
+ public void onServiceResolved(NsdServiceInfo resolved) {
85
+ if (!finished[0]) {
86
+ String sName = resolved.getServiceName();
87
+
88
+ // --- LÓGICA ANTI-GHOSTING ---
89
+ // Intentamos sacar la IP del nombre primero (es la fuente de verdad)
90
+ String ipFromName = "";
91
+ if (sName.contains("-")) {
92
+ ipFromName = sName.substring(sName.lastIndexOf("-") + 1);
93
+ }
94
+
95
+ // Si el nombre no tiene IP, usamos la resuelta por el sistema
96
+ String resolvedIp = resolved.getHost().getHostAddress();
97
+ if (resolvedIp.startsWith("/")) resolvedIp = resolvedIp.substring(1);
98
+
99
+ // Decisión final de IP
100
+ String finalIp = (!ipFromName.isEmpty()) ? ipFromName : resolvedIp;
101
+
102
+ // IMPORTANTE: Si la IP resuelta es 0.0.0.0 o vacía, ignoramos este evento
103
+ if (finalIp.equals("0.0.0.0") || finalIp.isEmpty()) return;
104
+
105
+ finished[0] = true;
106
+ stopDiscovery();
107
+
108
+ JSObject res = new JSObject();
109
+ JSObject meta = new JSObject();
110
+ for (Map.Entry<String, byte[]> entry : resolved.getAttributes().entrySet()) {
111
+ meta.put(entry.getKey(), new String(entry.getValue(), StandardCharsets.UTF_8));
112
+ }
113
+
114
+ res.put("ip", finalIp);
115
+ res.put("port", resolved.getPort());
116
+ res.put("metadata", meta);
117
+
118
+ System.out.println("NSD_DEBUG: IP Final entregada al Cliente: " + finalIp);
119
+ callback.success(res);
120
+ }
121
+ }
122
+ });
123
+ }
124
+ }
125
+ @Override public void onStopDiscoveryFailed(String s, int i) {}
126
+ @Override public void onDiscoveryStopped(String s) {}
127
+ @Override public void onServiceLost(NsdServiceInfo s) {}
43
128
  };
44
129
 
45
- nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
130
+ nsdManager.discoverServices(cleanType, NsdManager.PROTOCOL_DNS_SD, activeDiscoveryListener);
131
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
132
+ if (!finished[0]) { finished[0]=true; stopDiscovery(); callback.error("TIMEOUT_ERROR"); }
133
+ }, timeout);
134
+ }
135
+
136
+ public void stopServer(Callback c) {
137
+ if (activeRegistrationListener != null) { try { nsdManager.unregisterService(activeRegistrationListener); } catch (Exception e) {} }
138
+ activeRegistrationListener = null;
139
+ if (multicastLock.isHeld()) multicastLock.release();
140
+ if (c != null) c.success(new JSObject());
46
141
  }
47
142
 
48
- public interface RegistrationCallback {
49
- void onSuccess();
50
- void onError(int error);
143
+ public void stopDiscovery() {
144
+ if (activeDiscoveryListener != null) {
145
+ try {
146
+ nsdManager.stopServiceDiscovery(activeDiscoveryListener);
147
+ System.out.println("NSD_DEBUG: Discovery detenido.");
148
+ } catch (Exception e) {
149
+ System.out.println("NSD_DEBUG: Error al detener discovery: " + e.getMessage());
150
+ }
151
+ activeDiscoveryListener = null;
152
+ }
153
+ // Liberamos el lock solo si se detiene por completo el proceso
154
+ if (multicastLock != null && multicastLock.isHeld()) {
155
+ multicastLock.release();
156
+ }
51
157
  }
52
158
  }