react-native-zeroconf 0.14.0 → 0.15.0
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 +21 -5
- package/android/build.gradle +0 -7
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +10 -3
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +3 -2
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +83 -22
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +43 -14
- package/dist/index.js +21 -9
- package/index.d.ts +91 -0
- package/ios/RNZeroconf/RNNetServiceSerializer.m +7 -3
- package/ios/RNZeroconf/RNZeroconf.m +8 -0
- package/package.json +3 -1
- package/src/index.js +26 -9
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Discover and publish network services using Zeroconf protocols (Bonjour, Avahi,
|
|
|
11
11
|
- **Cross-Platform**: Works on iOS and Android
|
|
12
12
|
- **Dual Android Implementation**: Choose between NSD (Android native) or DNSSD (embedded mDNSResponder)
|
|
13
13
|
- **Android 15+ Compatible**: Includes 16KB page size alignment (Google Play requirement starting November 1, 2025)
|
|
14
|
+
- **TypeScript**: Type definitions included
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -34,7 +35,7 @@ For manual installation, see the [wiki](https://github.com/balthazar/react-nativ
|
|
|
34
35
|
|
|
35
36
|
### Android Permissions
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
The library declares the following permissions in its own manifest, so they are merged into your app automatically:
|
|
38
39
|
|
|
39
40
|
```xml
|
|
40
41
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
@@ -82,6 +83,23 @@ setTimeout(() => {
|
|
|
82
83
|
}, 10000)
|
|
83
84
|
```
|
|
84
85
|
|
|
86
|
+
## TypeScript
|
|
87
|
+
|
|
88
|
+
Type definitions ship with the package, so `@types/react-native-zeroconf` is no longer needed (uninstall it to avoid conflicting types). Event listeners are typed by event name:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import Zeroconf, { ImplType, Service } from 'react-native-zeroconf'
|
|
92
|
+
|
|
93
|
+
const zeroconf = new Zeroconf()
|
|
94
|
+
|
|
95
|
+
zeroconf.on('resolved', (service: Service) => {
|
|
96
|
+
console.log(service.name, service.addresses, service.txt)
|
|
97
|
+
})
|
|
98
|
+
zeroconf.on('remove', name => console.log(`${name} left`))
|
|
99
|
+
|
|
100
|
+
zeroconf.scan('http', 'tcp', 'local.', ImplType.DNSSD)
|
|
101
|
+
```
|
|
102
|
+
|
|
85
103
|
## API Reference
|
|
86
104
|
|
|
87
105
|
### Constructor
|
|
@@ -118,12 +136,10 @@ Stop the current scan.
|
|
|
118
136
|
|
|
119
137
|
| Parameter | Type | Default | Description |
|
|
120
138
|
| ---------- | ------ | ------- | ---------------------------------------------- |
|
|
121
|
-
| `implType` | string |
|
|
139
|
+
| `implType` | string | last scan's | **Android only**: Which implementation to stop. Defaults to the one used by the last `scan()` |
|
|
122
140
|
|
|
123
141
|
```javascript
|
|
124
142
|
zeroconf.stop()
|
|
125
|
-
// or on Android with DNSSD:
|
|
126
|
-
zeroconf.stop('DNSSD')
|
|
127
143
|
```
|
|
128
144
|
|
|
129
145
|
#### `getServices()`
|
|
@@ -163,7 +179,7 @@ Remove a published service.
|
|
|
163
179
|
| Parameter | Type | Default | Description |
|
|
164
180
|
| ---------- | ------ | -------- | -------------------------------------- |
|
|
165
181
|
| `name` | string | required | Name of the service to unpublish |
|
|
166
|
-
| `implType` | string |
|
|
182
|
+
| `implType` | string | publish's | **Android only**: Which implementation. Defaults to the one the service was published with |
|
|
167
183
|
|
|
168
184
|
```javascript
|
|
169
185
|
zeroconf.unpublishService('MyWebServer')
|
package/android/build.gradle
CHANGED
|
@@ -18,11 +18,6 @@ android {
|
|
|
18
18
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
externalNativeBuild {
|
|
22
|
-
ndkBuild {
|
|
23
|
-
arguments "NDK_APPLICATION_MK:=src/main/jni/Application.mk"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
externalNativeBuild {
|
|
@@ -50,6 +45,4 @@ dependencies {
|
|
|
50
45
|
// RxJava2 for Rx2Dnssd (bundled from Discord's RxDNSSD fork)
|
|
51
46
|
api 'io.reactivex.rxjava2:rxjava:2.2.21'
|
|
52
47
|
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
|
53
|
-
// https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
|
|
54
|
-
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.11'
|
|
55
48
|
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
3
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
4
|
+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
5
|
+
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
|
2
6
|
</manifest>
|
|
@@ -5,9 +5,9 @@ import com.balthazargronon.RCTZeroconf.rx2dnssd.DnssdImpl;
|
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
6
|
import com.facebook.react.bridge.ReactContext;
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import java.util.ArrayList;
|
|
10
9
|
import java.util.HashMap;
|
|
10
|
+
import java.util.List;
|
|
11
11
|
import java.util.Map;
|
|
12
12
|
|
|
13
13
|
public class ZeroConfImplFactory {
|
|
@@ -25,7 +25,7 @@ public class ZeroConfImplFactory {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
public Zeroconf getZeroconf(String implType) {
|
|
28
|
-
if (
|
|
28
|
+
if (implType == null || implType.trim().isEmpty()) implType = ZeroConfImplFactory.NSD_IMPL;
|
|
29
29
|
return getOrCreateImpl(implType);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -46,4 +46,11 @@ public class ZeroConfImplFactory {
|
|
|
46
46
|
return zeroconfMap.get(implType);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Implementations that have already been created, without instantiating new ones
|
|
51
|
+
*/
|
|
52
|
+
public List<Zeroconf> getCreatedImpls() {
|
|
53
|
+
return new ArrayList<>(zeroconfMap.values());
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
}
|
|
@@ -104,8 +104,9 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
104
104
|
public void onCatalystInstanceDestroy() {
|
|
105
105
|
super.onCatalystInstanceDestroy();
|
|
106
106
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
for (Zeroconf impl : zeroConfFactory.getCreatedImpls()) {
|
|
108
|
+
impl.stop();
|
|
109
|
+
}
|
|
109
110
|
} catch (Throwable e) {
|
|
110
111
|
Log.e(getClass().getName(), e.getMessage(), e);
|
|
111
112
|
sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Exception During Catalyst Destroy: " + e.getMessage());
|
|
@@ -6,6 +6,9 @@ import android.net.nsd.NsdManager;
|
|
|
6
6
|
import android.net.nsd.NsdServiceInfo;
|
|
7
7
|
import android.net.wifi.WifiManager;
|
|
8
8
|
import android.os.Build;
|
|
9
|
+
import android.os.Handler;
|
|
10
|
+
import android.os.Looper;
|
|
11
|
+
import android.util.Log;
|
|
9
12
|
|
|
10
13
|
import com.balthazargronon.RCTZeroconf.Zeroconf;
|
|
11
14
|
import com.balthazargronon.RCTZeroconf.ZeroconfModule;
|
|
@@ -18,13 +21,17 @@ import com.facebook.react.bridge.WritableMap;
|
|
|
18
21
|
import com.facebook.react.bridge.WritableNativeArray;
|
|
19
22
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
20
23
|
|
|
21
|
-
import java.io.UnsupportedEncodingException;
|
|
22
24
|
import java.net.InetAddress;
|
|
23
|
-
import java.
|
|
25
|
+
import java.nio.charset.StandardCharsets;
|
|
26
|
+
import java.util.ArrayDeque;
|
|
24
27
|
import java.util.Locale;
|
|
25
28
|
import java.util.Map;
|
|
29
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
26
30
|
|
|
27
31
|
public class NsdServiceImpl implements Zeroconf {
|
|
32
|
+
private static final String TAG = "NsdServiceImpl";
|
|
33
|
+
private static final long RESOLVE_RETRY_DELAY_MS = 100;
|
|
34
|
+
|
|
28
35
|
private NsdManager mNsdManager;
|
|
29
36
|
private NsdManager.DiscoveryListener mDiscoveryListener;
|
|
30
37
|
private WifiManager.MulticastLock multicastLock;
|
|
@@ -32,10 +39,15 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
32
39
|
private ZeroconfModule zeroconfModule;
|
|
33
40
|
private ReactApplicationContext reactApplicationContext;
|
|
34
41
|
|
|
42
|
+
// NsdManager can only resolve one service at a time before API 34, so resolves are queued
|
|
43
|
+
private final ArrayDeque<NsdServiceInfo> mResolveQueue = new ArrayDeque<>();
|
|
44
|
+
private boolean mIsResolving = false;
|
|
45
|
+
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
|
46
|
+
|
|
35
47
|
public NsdServiceImpl(ZeroconfModule zeroconfModule, ReactApplicationContext reactApplicationContext) {
|
|
36
48
|
this.zeroconfModule = zeroconfModule;
|
|
37
49
|
this.reactApplicationContext = reactApplicationContext;
|
|
38
|
-
mPublishedServices = new
|
|
50
|
+
mPublishedServices = new ConcurrentHashMap<String, NsdManager.RegistrationListener>();
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
@Override
|
|
@@ -68,29 +80,29 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
68
80
|
|
|
69
81
|
@Override
|
|
70
82
|
public void onDiscoveryStarted(String serviceType) {
|
|
71
|
-
|
|
83
|
+
Log.d(TAG, "Discovery started");
|
|
72
84
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_START, null);
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
@Override
|
|
76
88
|
public void onDiscoveryStopped(String serviceType) {
|
|
77
|
-
|
|
89
|
+
Log.d(TAG, "Discovery stopped");
|
|
78
90
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_STOP, null);
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
@Override
|
|
82
94
|
public void onServiceFound(NsdServiceInfo serviceInfo) {
|
|
83
|
-
|
|
95
|
+
Log.d(TAG, "Service found");
|
|
84
96
|
WritableMap service = new WritableNativeMap();
|
|
85
97
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
86
98
|
|
|
87
99
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_FOUND, service);
|
|
88
|
-
|
|
100
|
+
enqueueResolve(serviceInfo);
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
@Override
|
|
92
104
|
public void onServiceLost(NsdServiceInfo serviceInfo) {
|
|
93
|
-
|
|
105
|
+
Log.d(TAG, "Service lost");
|
|
94
106
|
WritableMap service = new WritableNativeMap();
|
|
95
107
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
96
108
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_REMOVE, service);
|
|
@@ -103,8 +115,19 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
103
115
|
|
|
104
116
|
@Override
|
|
105
117
|
public void stop() {
|
|
106
|
-
if (mDiscoveryListener != null) {
|
|
107
|
-
|
|
118
|
+
if (mDiscoveryListener != null && mNsdManager != null) {
|
|
119
|
+
try {
|
|
120
|
+
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
|
|
121
|
+
} catch (IllegalArgumentException e) {
|
|
122
|
+
// Listener was never registered or already unregistered (e.g. discovery failed to start)
|
|
123
|
+
Log.w(TAG, "stopServiceDiscovery failed", e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
mHandler.removeCallbacksAndMessages(null);
|
|
128
|
+
synchronized (mResolveQueue) {
|
|
129
|
+
mResolveQueue.clear();
|
|
130
|
+
mIsResolving = false;
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
if (multicastLock != null) {
|
|
@@ -159,21 +182,60 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
159
182
|
return reactApplicationContext;
|
|
160
183
|
}
|
|
161
184
|
|
|
185
|
+
private void enqueueResolve(NsdServiceInfo serviceInfo) {
|
|
186
|
+
synchronized (mResolveQueue) {
|
|
187
|
+
mResolveQueue.add(serviceInfo);
|
|
188
|
+
}
|
|
189
|
+
resolveNext();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private void resolveNext() {
|
|
193
|
+
NsdServiceInfo next;
|
|
194
|
+
synchronized (mResolveQueue) {
|
|
195
|
+
if (mIsResolving || mResolveQueue.isEmpty()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
next = mResolveQueue.poll();
|
|
199
|
+
mIsResolving = true;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
getNsdManager().resolveService(next, new ZeroResolveListener());
|
|
203
|
+
} catch (Throwable e) {
|
|
204
|
+
Log.e(TAG, "resolveService failed", e);
|
|
205
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Resolving service failed: " + e.getMessage());
|
|
206
|
+
onResolveDone();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private void onResolveDone() {
|
|
211
|
+
synchronized (mResolveQueue) {
|
|
212
|
+
mIsResolving = false;
|
|
213
|
+
}
|
|
214
|
+
resolveNext();
|
|
215
|
+
}
|
|
216
|
+
|
|
162
217
|
private class ZeroResolveListener implements NsdManager.ResolveListener {
|
|
163
218
|
@Override
|
|
164
219
|
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
165
220
|
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
221
|
+
// Another resolve (possibly from another app or listener) is in flight: retry shortly
|
|
222
|
+
synchronized (mResolveQueue) {
|
|
223
|
+
mResolveQueue.addFirst(serviceInfo);
|
|
224
|
+
mIsResolving = false;
|
|
225
|
+
}
|
|
226
|
+
mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
|
|
227
|
+
return;
|
|
170
228
|
}
|
|
229
|
+
String error = "Resolving service failed with code: " + errorCode;
|
|
230
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
231
|
+
onResolveDone();
|
|
171
232
|
}
|
|
172
233
|
|
|
173
234
|
@Override
|
|
174
235
|
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
|
175
236
|
WritableMap service = serviceInfoToMap(serviceInfo);
|
|
176
237
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_RESOLVE, service);
|
|
238
|
+
onResolveDone();
|
|
177
239
|
}
|
|
178
240
|
}
|
|
179
241
|
|
|
@@ -194,7 +256,8 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
194
256
|
|
|
195
257
|
@Override
|
|
196
258
|
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
197
|
-
|
|
259
|
+
String error = "Registering service " + serviceInfo.getServiceName() + " failed with code: " + errorCode;
|
|
260
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
198
261
|
}
|
|
199
262
|
|
|
200
263
|
@Override
|
|
@@ -207,7 +270,8 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
207
270
|
|
|
208
271
|
@Override
|
|
209
272
|
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
210
|
-
|
|
273
|
+
String error = "Unregistering service " + serviceInfo.getServiceName() + " failed with code: " + errorCode;
|
|
274
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
211
275
|
}
|
|
212
276
|
}
|
|
213
277
|
|
|
@@ -234,13 +298,10 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
234
298
|
|
|
235
299
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
236
300
|
Map<String, byte[]> attributes = serviceInfo.getAttributes();
|
|
237
|
-
|
|
238
|
-
|
|
301
|
+
if (attributes != null) {
|
|
302
|
+
for (String key : attributes.keySet()) {
|
|
239
303
|
byte[] recordValue = attributes.get(key);
|
|
240
|
-
txtRecords.putString(String.format(Locale.getDefault(), "%s", key),
|
|
241
|
-
} catch (UnsupportedEncodingException e) {
|
|
242
|
-
String error = "Failed to encode txtRecord: " + e;
|
|
243
|
-
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
304
|
+
txtRecords.putString(String.format(Locale.getDefault(), "%s", key), recordValue != null ? new String(recordValue, StandardCharsets.UTF_8) : "");
|
|
244
305
|
}
|
|
245
306
|
}
|
|
246
307
|
}
|
|
@@ -26,11 +26,14 @@ import java.util.List;
|
|
|
26
26
|
|
|
27
27
|
import javax.annotation.Nullable;
|
|
28
28
|
|
|
29
|
+
import io.reactivex.Flowable;
|
|
29
30
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
30
31
|
import io.reactivex.disposables.Disposable;
|
|
31
32
|
import io.reactivex.schedulers.Schedulers;
|
|
32
33
|
|
|
33
34
|
public class DnssdImpl implements Zeroconf {
|
|
35
|
+
private static final String TAG = "DnssdImpl";
|
|
36
|
+
|
|
34
37
|
private Rx2Dnssd rxDnssd;
|
|
35
38
|
|
|
36
39
|
@Nullable
|
|
@@ -74,26 +77,49 @@ public class DnssdImpl implements Zeroconf {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
String serviceType = getServiceType(type, protocol);
|
|
77
|
-
Log.d(
|
|
80
|
+
Log.d(TAG, "Starting DNSSD scan for: " + serviceType);
|
|
78
81
|
|
|
79
82
|
// Emit start event
|
|
80
83
|
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_START, null);
|
|
81
84
|
|
|
82
85
|
browseDisposable = rxDnssd.browse(serviceType, "local.")
|
|
83
|
-
.
|
|
84
|
-
|
|
86
|
+
.doOnNext(bonjourService -> {
|
|
87
|
+
if (!bonjourService.isLost()) {
|
|
88
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_FOUND, serviceNameToMap(bonjourService));
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
// Resolve each service independently so a single failure doesn't end the whole browse
|
|
92
|
+
.flatMap(bonjourService -> bonjourService.isLost()
|
|
93
|
+
? Flowable.just(bonjourService)
|
|
94
|
+
: Flowable.just(bonjourService)
|
|
95
|
+
.compose(rxDnssd.resolve())
|
|
96
|
+
.compose(rxDnssd.queryRecords())
|
|
97
|
+
.onErrorResumeNext((Throwable throwable) -> {
|
|
98
|
+
Log.e(TAG, "Error resolving service: ", throwable);
|
|
99
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Resolving service failed: " + throwable.getMessage());
|
|
100
|
+
return Flowable.empty();
|
|
101
|
+
}))
|
|
85
102
|
.subscribeOn(Schedulers.io())
|
|
86
103
|
.observeOn(AndroidSchedulers.mainThread())
|
|
87
104
|
.subscribe(bonjourService -> {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
if (bonjourService.isLost()) {
|
|
106
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_REMOVE, serviceNameToMap(bonjourService));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_RESOLVE, serviceInfoToMap(bonjourService));
|
|
91
110
|
}, throwable -> {
|
|
92
|
-
Log.e(
|
|
93
|
-
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, throwable.getMessage());
|
|
111
|
+
Log.e(TAG, "Error browsing services: ", throwable);
|
|
112
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Browsing services failed: " + throwable.getMessage());
|
|
113
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_STOP, null);
|
|
94
114
|
});
|
|
95
115
|
}
|
|
96
116
|
|
|
117
|
+
private WritableMap serviceNameToMap(BonjourService serviceInfo) {
|
|
118
|
+
WritableMap service = new WritableNativeMap();
|
|
119
|
+
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
120
|
+
return service;
|
|
121
|
+
}
|
|
122
|
+
|
|
97
123
|
private String getServiceType(String type, String protocol) {
|
|
98
124
|
return String.format("_%s._%s", type, protocol);
|
|
99
125
|
}
|
|
@@ -102,10 +128,11 @@ public class DnssdImpl implements Zeroconf {
|
|
|
102
128
|
WritableMap service = new WritableNativeMap();
|
|
103
129
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
104
130
|
final List<InetAddress> hostList = serviceInfo.getInetAddresses();
|
|
105
|
-
final String
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
final String hostname = serviceInfo.getHostname();
|
|
132
|
+
final String fullServiceName = hostname != null
|
|
133
|
+
? hostname + serviceInfo.getRegType()
|
|
134
|
+
: serviceInfo.getServiceName();
|
|
135
|
+
service.putString(ZeroconfModule.KEY_SERVICE_HOST, hostname != null ? hostname : serviceInfo.getServiceName());
|
|
109
136
|
|
|
110
137
|
WritableArray addresses = new WritableNativeArray();
|
|
111
138
|
for (InetAddress host : hostList) {
|
|
@@ -170,12 +197,14 @@ public class DnssdImpl implements Zeroconf {
|
|
|
170
197
|
.subscribeOn(Schedulers.io())
|
|
171
198
|
.observeOn(AndroidSchedulers.mainThread())
|
|
172
199
|
.subscribe(bonjourService -> {
|
|
173
|
-
Log.i(
|
|
200
|
+
Log.i(TAG, "Registered service " + bonjourService.toString());
|
|
174
201
|
|
|
175
202
|
mPublishedServices.put(bs.getServiceName(), bs);
|
|
176
203
|
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_PUBLISHED, serviceInfoToMap(bonjourService));
|
|
177
204
|
}, throwable -> {
|
|
178
|
-
Log.e(
|
|
205
|
+
Log.e(TAG, "Error registering service: ", throwable);
|
|
206
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Registering service failed: " + throwable.getMessage());
|
|
207
|
+
mRegisteredDisposables.remove(name);
|
|
179
208
|
});
|
|
180
209
|
|
|
181
210
|
mRegisteredDisposables.put(name, registerDisposable);
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,13 @@ const ImplType = exports.ImplType = {
|
|
|
14
14
|
class Zeroconf extends _events.EventEmitter {
|
|
15
15
|
constructor(props) {
|
|
16
16
|
super(props);
|
|
17
|
+
if (!RNZeroconf) {
|
|
18
|
+
throw new Error('react-native-zeroconf: native module not found. Make sure the library is linked and the app rebuilt. Expo Go is not supported, use a development build instead.');
|
|
19
|
+
}
|
|
17
20
|
this._services = {};
|
|
18
21
|
this._publishedServices = {};
|
|
22
|
+
this._publishedImplTypes = {};
|
|
23
|
+
this._scanImplType = null;
|
|
19
24
|
this._dListeners = {};
|
|
20
25
|
this.addDeviceListeners();
|
|
21
26
|
}
|
|
@@ -103,6 +108,11 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
103
108
|
this._services = {};
|
|
104
109
|
this.emit('update');
|
|
105
110
|
if (_reactNative.Platform.OS === 'android') {
|
|
111
|
+
if (this._scanImplType && implType !== this._scanImplType) {
|
|
112
|
+
// Only one scan runs at a time, stop the one running on the other implementation
|
|
113
|
+
RNZeroconf.stop(this._scanImplType);
|
|
114
|
+
}
|
|
115
|
+
this._scanImplType = implType;
|
|
106
116
|
RNZeroconf.scan(type, protocol, domain, implType);
|
|
107
117
|
} else {
|
|
108
118
|
RNZeroconf.scan(type, protocol, domain);
|
|
@@ -110,9 +120,10 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
/**
|
|
113
|
-
* Stop current scan if any
|
|
123
|
+
* Stop current scan if any,
|
|
124
|
+
* Defaults to the implementation used by the last scan
|
|
114
125
|
*/
|
|
115
|
-
stop(implType = ImplType.NSD) {
|
|
126
|
+
stop(implType = this._scanImplType || ImplType.NSD) {
|
|
116
127
|
if (_reactNative.Platform.OS === 'android') {
|
|
117
128
|
RNZeroconf.stop(implType);
|
|
118
129
|
} else {
|
|
@@ -124,21 +135,22 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
124
135
|
* Publish a service
|
|
125
136
|
*/
|
|
126
137
|
publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
|
|
127
|
-
|
|
128
|
-
Object.entries(txt).map(([key, value]) => txt[key] = value.toString());
|
|
129
|
-
}
|
|
138
|
+
const txtRecord = Object.fromEntries(Object.entries(txt || {}).map(([key, value]) => [key, String(value)]));
|
|
130
139
|
if (_reactNative.Platform.OS === 'android') {
|
|
131
|
-
|
|
140
|
+
this._publishedImplTypes[name] = implType;
|
|
141
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType);
|
|
132
142
|
} else {
|
|
133
|
-
RNZeroconf.registerService(type, protocol, domain, name, port,
|
|
143
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord);
|
|
134
144
|
}
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
/**
|
|
138
|
-
* Unpublish a service
|
|
148
|
+
* Unpublish a service,
|
|
149
|
+
* Defaults to the implementation the service was published with
|
|
139
150
|
*/
|
|
140
|
-
unpublishService(name, implType = ImplType.NSD) {
|
|
151
|
+
unpublishService(name, implType = this._publishedImplTypes[name] || ImplType.NSD) {
|
|
141
152
|
if (_reactNative.Platform.OS === 'android') {
|
|
153
|
+
delete this._publishedImplTypes[name];
|
|
142
154
|
RNZeroconf.unregisterService(name, implType);
|
|
143
155
|
} else {
|
|
144
156
|
RNZeroconf.unregisterService(name);
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export declare const ImplType: {
|
|
2
|
+
readonly NSD: 'NSD'
|
|
3
|
+
readonly DNSSD: 'DNSSD'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Android only: which discovery implementation to use */
|
|
7
|
+
export type ImplType = (typeof ImplType)[keyof typeof ImplType]
|
|
8
|
+
|
|
9
|
+
export interface Service {
|
|
10
|
+
/** Human-readable service name */
|
|
11
|
+
name: string
|
|
12
|
+
/** Full service name, e.g. `XeroxPrinter.local._http._tcp.` */
|
|
13
|
+
fullName: string
|
|
14
|
+
/** Hostname, e.g. `XeroxPrinter.local.` */
|
|
15
|
+
host: string
|
|
16
|
+
port: number
|
|
17
|
+
/** IPv4 and/or IPv6 addresses */
|
|
18
|
+
addresses: string[]
|
|
19
|
+
/** TXT record attributes */
|
|
20
|
+
txt: Record<string, string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ZeroconfEvents {
|
|
24
|
+
start: () => void
|
|
25
|
+
stop: () => void
|
|
26
|
+
/** Service found, before resolution */
|
|
27
|
+
found: (name: string) => void
|
|
28
|
+
/** Service fully resolved with network info */
|
|
29
|
+
resolved: (service: Service) => void
|
|
30
|
+
/** Service removed from the network */
|
|
31
|
+
remove: (name: string) => void
|
|
32
|
+
/** Services list changed */
|
|
33
|
+
update: () => void
|
|
34
|
+
error: (error: Error) => void
|
|
35
|
+
published: (service: Service) => void
|
|
36
|
+
unpublished: (service: Service) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Extends the `events` package EventEmitter */
|
|
40
|
+
export default class Zeroconf {
|
|
41
|
+
constructor()
|
|
42
|
+
|
|
43
|
+
on<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
44
|
+
once<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
45
|
+
addListener<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
46
|
+
removeListener<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
47
|
+
off<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
48
|
+
removeAllListeners(event?: keyof ZeroconfEvents): this
|
|
49
|
+
listenerCount(event: keyof ZeroconfEvents): number
|
|
50
|
+
|
|
51
|
+
/** Add the native event listeners (called automatically in the constructor) */
|
|
52
|
+
addDeviceListeners(): void
|
|
53
|
+
|
|
54
|
+
/** Remove the native event listeners, call it when you are done with the instance */
|
|
55
|
+
removeDeviceListeners(): void
|
|
56
|
+
|
|
57
|
+
/** All the services found so far, keyed by name */
|
|
58
|
+
getServices(): Record<string, Service>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan for services, defaults to `_http._tcp.` on the `local.` domain
|
|
62
|
+
* @param implType Android only, defaults to `NSD`
|
|
63
|
+
*/
|
|
64
|
+
scan(type?: string, protocol?: string, domain?: string, implType?: ImplType): void
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stop the current scan
|
|
68
|
+
* @param implType Android only, defaults to the implementation used by the last scan
|
|
69
|
+
*/
|
|
70
|
+
stop(implType?: ImplType): void
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Publish a service. Type and protocol are passed without underscores, e.g. `('http', 'tcp', ...)`
|
|
74
|
+
* @param implType Android only, defaults to `NSD`
|
|
75
|
+
*/
|
|
76
|
+
publishService(
|
|
77
|
+
type: string,
|
|
78
|
+
protocol: string,
|
|
79
|
+
domain: string | undefined,
|
|
80
|
+
name: string,
|
|
81
|
+
port: number,
|
|
82
|
+
txt?: Record<string, string | number | boolean>,
|
|
83
|
+
implType?: ImplType,
|
|
84
|
+
): void
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Unpublish a service
|
|
88
|
+
* @param implType Android only, defaults to the implementation the service was published with
|
|
89
|
+
*/
|
|
90
|
+
unpublishService(name: string, implType?: ImplType): void
|
|
91
|
+
}
|
|
@@ -35,9 +35,13 @@ const NSString *kRNServiceTxtRecords = @"txt";
|
|
|
35
35
|
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
|
|
36
36
|
for (NSString *key in txtRecordDict) {
|
|
37
37
|
@try{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
NSData *value = txtRecordDict[key];
|
|
39
|
+
NSString *string = [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding];
|
|
40
|
+
if (string == nil) {
|
|
41
|
+
// Not valid UTF-8, fall back to a lossless 8-bit decoding
|
|
42
|
+
string = [[NSString alloc] initWithData:value encoding:NSISOLatin1StringEncoding];
|
|
43
|
+
}
|
|
44
|
+
dict[key] = string;
|
|
41
45
|
}
|
|
42
46
|
@catch(NSException *exception){
|
|
43
47
|
NSLog(@"%@", exception);
|
|
@@ -106,6 +106,14 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// Stop any pending resolve for the removed service
|
|
110
|
+
NSNetService *resolving = self.resolvingServices[service.name];
|
|
111
|
+
if (resolving) {
|
|
112
|
+
resolving.delegate = nil;
|
|
113
|
+
[resolving stop];
|
|
114
|
+
[self.resolvingServices removeObjectForKey:service.name];
|
|
115
|
+
}
|
|
116
|
+
|
|
109
117
|
NSDictionary *serviceInfo = [RNNetServiceSerializer serializeServiceToDictionary:service resolved:NO];
|
|
110
118
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfRemove" body:serviceInfo];
|
|
111
119
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-zeroconf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A Zeroconf discovery utility for react-native",
|
|
5
5
|
"main": "dist",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"lint": "eslint src/*.js",
|
|
8
9
|
"format": "prettier --write src/*.js",
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
"files": [
|
|
16
17
|
"src",
|
|
17
18
|
"dist",
|
|
19
|
+
"index.d.ts",
|
|
18
20
|
"android",
|
|
19
21
|
"ios",
|
|
20
22
|
"react-native-zeroconf.podspec",
|
package/src/index.js
CHANGED
|
@@ -12,8 +12,16 @@ export default class Zeroconf extends EventEmitter {
|
|
|
12
12
|
constructor(props) {
|
|
13
13
|
super(props)
|
|
14
14
|
|
|
15
|
+
if (!RNZeroconf) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'react-native-zeroconf: native module not found. Make sure the library is linked and the app rebuilt. Expo Go is not supported, use a development build instead.',
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
this._services = {}
|
|
16
22
|
this._publishedServices = {}
|
|
23
|
+
this._publishedImplTypes = {}
|
|
24
|
+
this._scanImplType = null
|
|
17
25
|
this._dListeners = {}
|
|
18
26
|
|
|
19
27
|
this.addDeviceListeners()
|
|
@@ -122,6 +130,11 @@ export default class Zeroconf extends EventEmitter {
|
|
|
122
130
|
this._services = {}
|
|
123
131
|
this.emit('update')
|
|
124
132
|
if (Platform.OS === 'android') {
|
|
133
|
+
if (this._scanImplType && implType !== this._scanImplType) {
|
|
134
|
+
// Only one scan runs at a time, stop the one running on the other implementation
|
|
135
|
+
RNZeroconf.stop(this._scanImplType)
|
|
136
|
+
}
|
|
137
|
+
this._scanImplType = implType
|
|
125
138
|
RNZeroconf.scan(type, protocol, domain, implType)
|
|
126
139
|
} else {
|
|
127
140
|
RNZeroconf.scan(type, protocol, domain)
|
|
@@ -129,9 +142,10 @@ export default class Zeroconf extends EventEmitter {
|
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
/**
|
|
132
|
-
* Stop current scan if any
|
|
145
|
+
* Stop current scan if any,
|
|
146
|
+
* Defaults to the implementation used by the last scan
|
|
133
147
|
*/
|
|
134
|
-
stop(implType = ImplType.NSD) {
|
|
148
|
+
stop(implType = this._scanImplType || ImplType.NSD) {
|
|
135
149
|
if (Platform.OS === 'android') {
|
|
136
150
|
RNZeroconf.stop(implType)
|
|
137
151
|
} else {
|
|
@@ -143,21 +157,24 @@ export default class Zeroconf extends EventEmitter {
|
|
|
143
157
|
* Publish a service
|
|
144
158
|
*/
|
|
145
159
|
publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
|
|
146
|
-
|
|
147
|
-
Object.entries(txt).map(([key, value]) =>
|
|
148
|
-
|
|
160
|
+
const txtRecord = Object.fromEntries(
|
|
161
|
+
Object.entries(txt || {}).map(([key, value]) => [key, String(value)]),
|
|
162
|
+
)
|
|
149
163
|
if (Platform.OS === 'android') {
|
|
150
|
-
|
|
164
|
+
this._publishedImplTypes[name] = implType
|
|
165
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType)
|
|
151
166
|
} else {
|
|
152
|
-
RNZeroconf.registerService(type, protocol, domain, name, port,
|
|
167
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord)
|
|
153
168
|
}
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
/**
|
|
157
|
-
* Unpublish a service
|
|
172
|
+
* Unpublish a service,
|
|
173
|
+
* Defaults to the implementation the service was published with
|
|
158
174
|
*/
|
|
159
|
-
unpublishService(name, implType = ImplType.NSD) {
|
|
175
|
+
unpublishService(name, implType = this._publishedImplTypes[name] || ImplType.NSD) {
|
|
160
176
|
if (Platform.OS === 'android') {
|
|
177
|
+
delete this._publishedImplTypes[name]
|
|
161
178
|
RNZeroconf.unregisterService(name, implType)
|
|
162
179
|
} else {
|
|
163
180
|
RNZeroconf.unregisterService(name)
|