react-native-zeroconf 0.14.0 → 0.15.1
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 +37 -7
- 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/Zeroconf.java +10 -2
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +26 -10
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +139 -34
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +60 -23
- package/dist/index.js +48 -11
- package/index.d.ts +101 -0
- package/ios/RNZeroconf/RNNetServiceSerializer.m +7 -3
- package/ios/RNZeroconf/RNZeroconf.h +2 -1
- package/ios/RNZeroconf/RNZeroconf.m +47 -4
- package/package.json +3 -1
- package/src/index.js +47 -11
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()`
|
|
@@ -146,7 +162,7 @@ Publish a service on the network.
|
|
|
146
162
|
| `domain` | string | `'local.'` | Domain |
|
|
147
163
|
| `name` | string | required | Service name (should be unique) |
|
|
148
164
|
| `port` | number | required | Port number |
|
|
149
|
-
| `txt` | object | `{}`
|
|
165
|
+
| `txt` | object \| array | `{}` | TXT record key-value pairs, or `[key, value]` pairs to control the order |
|
|
150
166
|
| `implType` | string | `'NSD'` | **Android only**: `'NSD'` or `'DNSSD'` |
|
|
151
167
|
|
|
152
168
|
```javascript
|
|
@@ -154,8 +170,18 @@ zeroconf.publishService('http', 'tcp', 'local.', 'MyWebServer', 8080, {
|
|
|
154
170
|
path: '/api',
|
|
155
171
|
version: '1.0',
|
|
156
172
|
})
|
|
173
|
+
|
|
174
|
+
// TXT records are published in the order given. Use pairs when order matters and keys look like numbers
|
|
175
|
+
zeroconf.publishService('http', 'tcp', 'local.', 'MyWebServer', 8080, [
|
|
176
|
+
['txtvers', '1'],
|
|
177
|
+
['path', '/api'],
|
|
178
|
+
])
|
|
157
179
|
```
|
|
158
180
|
|
|
181
|
+
TXT order is preserved on iOS and with `DNSSD` on Android. With `NSD`, the order depends on the Android system.
|
|
182
|
+
|
|
183
|
+
Published services are unpublished when the React Native instance is torn down (e.g. on reload). If the app process is killed, it can't announce anything: other devices only drop the service once its mDNS records expire.
|
|
184
|
+
|
|
159
185
|
#### `unpublishService(name, implType)`
|
|
160
186
|
|
|
161
187
|
Remove a published service.
|
|
@@ -163,7 +189,7 @@ Remove a published service.
|
|
|
163
189
|
| Parameter | Type | Default | Description |
|
|
164
190
|
| ---------- | ------ | -------- | -------------------------------------- |
|
|
165
191
|
| `name` | string | required | Name of the service to unpublish |
|
|
166
|
-
| `implType` | string |
|
|
192
|
+
| `implType` | string | publish's | **Android only**: Which implementation. Defaults to the one the service was published with |
|
|
167
193
|
|
|
168
194
|
```javascript
|
|
169
195
|
zeroconf.unpublishService('MyWebServer')
|
|
@@ -219,10 +245,12 @@ useEffect(() => {
|
|
|
219
245
|
fullName: 'XeroxPrinter._http._tcp.local.', // Full service name
|
|
220
246
|
host: 'XeroxPrinter.local.', // Hostname
|
|
221
247
|
port: 8080, // Port number
|
|
222
|
-
addresses: [ // IP addresses
|
|
248
|
+
addresses: [ // IP addresses, IPv4 first
|
|
223
249
|
'192.168.1.23',
|
|
224
250
|
'fe80::aebc:123:ffff:abcd'
|
|
225
251
|
],
|
|
252
|
+
ipv4: ['192.168.1.23'], // IPv4 addresses only
|
|
253
|
+
ipv6: ['fe80::aebc:123:ffff:abcd'], // IPv6 addresses only
|
|
226
254
|
txt: { // TXT record attributes
|
|
227
255
|
path: '/status',
|
|
228
256
|
color: 'yes'
|
|
@@ -230,6 +258,8 @@ useEffect(() => {
|
|
|
230
258
|
}
|
|
231
259
|
```
|
|
232
260
|
|
|
261
|
+
On Android with `NSD`, `host` is the mDNS hostname on Android 16+. Older versions don't expose it, so `host` is usually the IP address (use `DNSSD` if you need the hostname there). `NSD` returns all addresses on Android 14+, and a single one before.
|
|
262
|
+
|
|
233
263
|
## Android Implementation Types
|
|
234
264
|
|
|
235
265
|
This library supports two implementations on Android:
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
package com.balthazargronon.RCTZeroconf;
|
|
2
2
|
|
|
3
|
-
import com.facebook.react.bridge.
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
4
4
|
|
|
5
5
|
public interface Zeroconf {
|
|
6
6
|
|
|
@@ -10,5 +10,13 @@ public interface Zeroconf {
|
|
|
10
10
|
|
|
11
11
|
public void unregisterService(String serviceName);
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param txt TXT records as ordered [key, value] pairs
|
|
15
|
+
*/
|
|
16
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Unregister every service published through this implementation
|
|
20
|
+
*/
|
|
21
|
+
void unregisterAllServices();
|
|
14
22
|
}
|
|
@@ -7,7 +7,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
7
7
|
import com.facebook.react.bridge.ReactContext;
|
|
8
8
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
9
9
|
import com.facebook.react.bridge.ReactMethod;
|
|
10
|
-
import com.facebook.react.bridge.
|
|
10
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
11
11
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
12
12
|
|
|
13
13
|
import javax.annotation.Nullable;
|
|
@@ -73,7 +73,7 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
@ReactMethod
|
|
76
|
-
public void registerService(String type, String protocol, String domain, String name, int port,
|
|
76
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, String implType) {
|
|
77
77
|
try {
|
|
78
78
|
getZeroconfImpl(implType).registerService(type, protocol, domain, name, port, txt);
|
|
79
79
|
} catch (Throwable e) {
|
|
@@ -95,20 +95,36 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
95
95
|
public void sendEvent(ReactContext reactContext,
|
|
96
96
|
String eventName,
|
|
97
97
|
@Nullable Object params) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
try {
|
|
99
|
+
reactContext
|
|
100
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
101
|
+
.emit(eventName, params);
|
|
102
|
+
} catch (Throwable e) {
|
|
103
|
+
// The JS side may already be gone, e.g. callbacks arriving during teardown
|
|
104
|
+
Log.w(getClass().getName(), "Could not send " + eventName + ": " + e.getMessage());
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
// Called on teardown by current React Native versions.
|
|
109
|
+
// No @Override so it still compiles against versions that don't declare it.
|
|
110
|
+
public void invalidate() {
|
|
111
|
+
teardown();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Called on teardown by older React Native versions, deprecated in favor of invalidate().
|
|
115
|
+
// No @Override so it still compiles once it is removed.
|
|
104
116
|
public void onCatalystInstanceDestroy() {
|
|
105
|
-
|
|
117
|
+
teardown();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private void teardown() {
|
|
106
121
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
for (Zeroconf impl : zeroConfFactory.getCreatedImpls()) {
|
|
123
|
+
impl.stop();
|
|
124
|
+
impl.unregisterAllServices();
|
|
125
|
+
}
|
|
109
126
|
} catch (Throwable e) {
|
|
110
127
|
Log.e(getClass().getName(), e.getMessage(), e);
|
|
111
|
-
sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Exception During Catalyst Destroy: " + e.getMessage());
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
130
|
}
|
|
@@ -6,25 +6,34 @@ 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;
|
|
12
15
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
13
16
|
|
|
14
|
-
import com.facebook.react.bridge.
|
|
15
|
-
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
17
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
16
18
|
import com.facebook.react.bridge.WritableArray;
|
|
17
19
|
import com.facebook.react.bridge.WritableMap;
|
|
18
20
|
import com.facebook.react.bridge.WritableNativeArray;
|
|
19
21
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
20
22
|
|
|
21
|
-
import java.io.UnsupportedEncodingException;
|
|
22
23
|
import java.net.InetAddress;
|
|
23
|
-
import java.
|
|
24
|
+
import java.nio.charset.StandardCharsets;
|
|
25
|
+
import java.util.ArrayDeque;
|
|
26
|
+
import java.util.ArrayList;
|
|
27
|
+
import java.util.Collections;
|
|
28
|
+
import java.util.List;
|
|
24
29
|
import java.util.Locale;
|
|
25
30
|
import java.util.Map;
|
|
31
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
26
32
|
|
|
27
33
|
public class NsdServiceImpl implements Zeroconf {
|
|
34
|
+
private static final String TAG = "NsdServiceImpl";
|
|
35
|
+
private static final long RESOLVE_RETRY_DELAY_MS = 100;
|
|
36
|
+
|
|
28
37
|
private NsdManager mNsdManager;
|
|
29
38
|
private NsdManager.DiscoveryListener mDiscoveryListener;
|
|
30
39
|
private WifiManager.MulticastLock multicastLock;
|
|
@@ -32,10 +41,15 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
32
41
|
private ZeroconfModule zeroconfModule;
|
|
33
42
|
private ReactApplicationContext reactApplicationContext;
|
|
34
43
|
|
|
44
|
+
// NsdManager can only resolve one service at a time before API 34, so resolves are queued
|
|
45
|
+
private final ArrayDeque<NsdServiceInfo> mResolveQueue = new ArrayDeque<>();
|
|
46
|
+
private boolean mIsResolving = false;
|
|
47
|
+
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
|
48
|
+
|
|
35
49
|
public NsdServiceImpl(ZeroconfModule zeroconfModule, ReactApplicationContext reactApplicationContext) {
|
|
36
50
|
this.zeroconfModule = zeroconfModule;
|
|
37
51
|
this.reactApplicationContext = reactApplicationContext;
|
|
38
|
-
mPublishedServices = new
|
|
52
|
+
mPublishedServices = new ConcurrentHashMap<String, NsdManager.RegistrationListener>();
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
@Override
|
|
@@ -68,29 +82,29 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
68
82
|
|
|
69
83
|
@Override
|
|
70
84
|
public void onDiscoveryStarted(String serviceType) {
|
|
71
|
-
|
|
85
|
+
Log.d(TAG, "Discovery started");
|
|
72
86
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_START, null);
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
@Override
|
|
76
90
|
public void onDiscoveryStopped(String serviceType) {
|
|
77
|
-
|
|
91
|
+
Log.d(TAG, "Discovery stopped");
|
|
78
92
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_STOP, null);
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
@Override
|
|
82
96
|
public void onServiceFound(NsdServiceInfo serviceInfo) {
|
|
83
|
-
|
|
97
|
+
Log.d(TAG, "Service found");
|
|
84
98
|
WritableMap service = new WritableNativeMap();
|
|
85
99
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
86
100
|
|
|
87
101
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_FOUND, service);
|
|
88
|
-
|
|
102
|
+
enqueueResolve(serviceInfo);
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
@Override
|
|
92
106
|
public void onServiceLost(NsdServiceInfo serviceInfo) {
|
|
93
|
-
|
|
107
|
+
Log.d(TAG, "Service lost");
|
|
94
108
|
WritableMap service = new WritableNativeMap();
|
|
95
109
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
96
110
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_REMOVE, service);
|
|
@@ -103,8 +117,19 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
103
117
|
|
|
104
118
|
@Override
|
|
105
119
|
public void stop() {
|
|
106
|
-
if (mDiscoveryListener != null) {
|
|
107
|
-
|
|
120
|
+
if (mDiscoveryListener != null && mNsdManager != null) {
|
|
121
|
+
try {
|
|
122
|
+
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
|
|
123
|
+
} catch (IllegalArgumentException e) {
|
|
124
|
+
// Listener was never registered or already unregistered (e.g. discovery failed to start)
|
|
125
|
+
Log.w(TAG, "stopServiceDiscovery failed", e);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
mHandler.removeCallbacksAndMessages(null);
|
|
130
|
+
synchronized (mResolveQueue) {
|
|
131
|
+
mResolveQueue.clear();
|
|
132
|
+
mIsResolving = false;
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
if (multicastLock != null) {
|
|
@@ -116,7 +141,7 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
116
141
|
}
|
|
117
142
|
|
|
118
143
|
@Override
|
|
119
|
-
public void registerService(String type, String protocol, String domain, String name, int port,
|
|
144
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt) {
|
|
120
145
|
String serviceType = String.format("_%s._%s.", type, protocol);
|
|
121
146
|
|
|
122
147
|
final NsdManager nsdManager = this.getNsdManager();
|
|
@@ -125,10 +150,9 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
125
150
|
serviceInfo.setServiceType(serviceType);
|
|
126
151
|
serviceInfo.setPort(port);
|
|
127
152
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
serviceInfo.setAttribute(key, txt.getString(key));
|
|
153
|
+
for (int i = 0; i < txt.size(); i++) {
|
|
154
|
+
ReadableArray pair = txt.getArray(i);
|
|
155
|
+
serviceInfo.setAttribute(pair.getString(0), pair.getString(1));
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
nsdManager.registerService(
|
|
@@ -148,6 +172,13 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
|
|
175
|
+
@Override
|
|
176
|
+
public void unregisterAllServices() {
|
|
177
|
+
for (String serviceName : new ArrayList<>(mPublishedServices.keySet())) {
|
|
178
|
+
unregisterService(serviceName);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
151
182
|
private NsdManager getNsdManager() {
|
|
152
183
|
if (mNsdManager == null) {
|
|
153
184
|
mNsdManager = (NsdManager) getReactApplicationContext().getSystemService(Context.NSD_SERVICE);
|
|
@@ -159,21 +190,60 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
159
190
|
return reactApplicationContext;
|
|
160
191
|
}
|
|
161
192
|
|
|
193
|
+
private void enqueueResolve(NsdServiceInfo serviceInfo) {
|
|
194
|
+
synchronized (mResolveQueue) {
|
|
195
|
+
mResolveQueue.add(serviceInfo);
|
|
196
|
+
}
|
|
197
|
+
resolveNext();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private void resolveNext() {
|
|
201
|
+
NsdServiceInfo next;
|
|
202
|
+
synchronized (mResolveQueue) {
|
|
203
|
+
if (mIsResolving || mResolveQueue.isEmpty()) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
next = mResolveQueue.poll();
|
|
207
|
+
mIsResolving = true;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
getNsdManager().resolveService(next, new ZeroResolveListener());
|
|
211
|
+
} catch (Throwable e) {
|
|
212
|
+
Log.e(TAG, "resolveService failed", e);
|
|
213
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Resolving service failed: " + e.getMessage());
|
|
214
|
+
onResolveDone();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private void onResolveDone() {
|
|
219
|
+
synchronized (mResolveQueue) {
|
|
220
|
+
mIsResolving = false;
|
|
221
|
+
}
|
|
222
|
+
resolveNext();
|
|
223
|
+
}
|
|
224
|
+
|
|
162
225
|
private class ZeroResolveListener implements NsdManager.ResolveListener {
|
|
163
226
|
@Override
|
|
164
227
|
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
165
228
|
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
229
|
+
// Another resolve (possibly from another app or listener) is in flight: retry shortly
|
|
230
|
+
synchronized (mResolveQueue) {
|
|
231
|
+
mResolveQueue.addFirst(serviceInfo);
|
|
232
|
+
mIsResolving = false;
|
|
233
|
+
}
|
|
234
|
+
mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
|
|
235
|
+
return;
|
|
170
236
|
}
|
|
237
|
+
String error = "Resolving service failed with code: " + errorCode;
|
|
238
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
239
|
+
onResolveDone();
|
|
171
240
|
}
|
|
172
241
|
|
|
173
242
|
@Override
|
|
174
243
|
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
|
175
244
|
WritableMap service = serviceInfoToMap(serviceInfo);
|
|
176
245
|
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_RESOLVE, service);
|
|
246
|
+
onResolveDone();
|
|
177
247
|
}
|
|
178
248
|
}
|
|
179
249
|
|
|
@@ -194,7 +264,8 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
194
264
|
|
|
195
265
|
@Override
|
|
196
266
|
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
197
|
-
|
|
267
|
+
String error = "Registering service " + serviceInfo.getServiceName() + " failed with code: " + errorCode;
|
|
268
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
198
269
|
}
|
|
199
270
|
|
|
200
271
|
@Override
|
|
@@ -207,23 +278,60 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
207
278
|
|
|
208
279
|
@Override
|
|
209
280
|
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
210
|
-
|
|
281
|
+
String error = "Unregistering service " + serviceInfo.getServiceName() + " failed with code: " + errorCode;
|
|
282
|
+
zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, error);
|
|
211
283
|
}
|
|
212
284
|
}
|
|
213
285
|
|
|
286
|
+
/**
|
|
287
|
+
* All the addresses of the service on Android 14+, only one before
|
|
288
|
+
*/
|
|
289
|
+
@SuppressWarnings("deprecation")
|
|
290
|
+
private static List<InetAddress> getHostAddresses(NsdServiceInfo serviceInfo) {
|
|
291
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
292
|
+
return serviceInfo.getHostAddresses();
|
|
293
|
+
}
|
|
294
|
+
InetAddress host = serviceInfo.getHost();
|
|
295
|
+
return host == null ? Collections.<InetAddress>emptyList() : Collections.singletonList(host);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The mDNS hostname (e.g. "MyHost.local.") when available.
|
|
300
|
+
* NsdServiceInfo.getHostname() is API 36 (or backported through the Connectivity module),
|
|
301
|
+
* it is called through reflection so the library still compiles against older SDKs.
|
|
302
|
+
*/
|
|
303
|
+
private static String getMdnsHostname(NsdServiceInfo serviceInfo) {
|
|
304
|
+
try {
|
|
305
|
+
Object hostname = NsdServiceInfo.class.getMethod("getHostname").invoke(serviceInfo);
|
|
306
|
+
if (hostname instanceof String && !((String) hostname).isEmpty()) {
|
|
307
|
+
return hostname + ".local.";
|
|
308
|
+
}
|
|
309
|
+
} catch (Exception e) {
|
|
310
|
+
// Not available on this device
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
214
315
|
private WritableMap serviceInfoToMap(NsdServiceInfo serviceInfo) {
|
|
215
316
|
WritableMap service = new WritableNativeMap();
|
|
216
317
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
217
|
-
final InetAddress
|
|
318
|
+
final List<InetAddress> hostAddresses = getHostAddresses(serviceInfo);
|
|
218
319
|
final String fullServiceName;
|
|
219
|
-
if (
|
|
320
|
+
if (hostAddresses.isEmpty()) {
|
|
220
321
|
fullServiceName = serviceInfo.getServiceName();
|
|
221
322
|
} else {
|
|
222
|
-
|
|
223
|
-
|
|
323
|
+
String hostname = getMdnsHostname(serviceInfo);
|
|
324
|
+
if (hostname == null) {
|
|
325
|
+
// No hostname API before Android 16, falls back to a reverse lookup (often the IP)
|
|
326
|
+
hostname = hostAddresses.get(0).getHostName();
|
|
327
|
+
}
|
|
328
|
+
fullServiceName = hostname + serviceInfo.getServiceType();
|
|
329
|
+
service.putString(ZeroconfModule.KEY_SERVICE_HOST, hostname);
|
|
224
330
|
|
|
225
331
|
WritableArray addresses = new WritableNativeArray();
|
|
226
|
-
|
|
332
|
+
for (InetAddress address : hostAddresses) {
|
|
333
|
+
addresses.pushString(address.getHostAddress());
|
|
334
|
+
}
|
|
227
335
|
|
|
228
336
|
service.putArray(ZeroconfModule.KEY_SERVICE_ADDRESSES, addresses);
|
|
229
337
|
}
|
|
@@ -234,13 +342,10 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
234
342
|
|
|
235
343
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
236
344
|
Map<String, byte[]> attributes = serviceInfo.getAttributes();
|
|
237
|
-
|
|
238
|
-
|
|
345
|
+
if (attributes != null) {
|
|
346
|
+
for (String key : attributes.keySet()) {
|
|
239
347
|
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);
|
|
348
|
+
txtRecords.putString(String.format(Locale.getDefault(), "%s", key), recordValue != null ? new String(recordValue, StandardCharsets.UTF_8) : "");
|
|
244
349
|
}
|
|
245
350
|
}
|
|
246
351
|
}
|
|
@@ -8,8 +8,7 @@ import android.util.Log;
|
|
|
8
8
|
import com.balthazargronon.RCTZeroconf.Zeroconf;
|
|
9
9
|
import com.balthazargronon.RCTZeroconf.ZeroconfModule;
|
|
10
10
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
-
import com.facebook.react.bridge.
|
|
12
|
-
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
11
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
13
12
|
import com.facebook.react.bridge.WritableArray;
|
|
14
13
|
import com.facebook.react.bridge.WritableMap;
|
|
15
14
|
import com.facebook.react.bridge.WritableNativeArray;
|
|
@@ -19,18 +18,23 @@ import com.github.druk.rx2dnssd.Rx2Dnssd;
|
|
|
19
18
|
import com.github.druk.rx2dnssd.Rx2DnssdEmbedded;
|
|
20
19
|
|
|
21
20
|
import java.net.InetAddress;
|
|
21
|
+
import java.util.ArrayList;
|
|
22
22
|
import java.util.HashMap;
|
|
23
|
+
import java.util.LinkedHashMap;
|
|
23
24
|
import java.util.Locale;
|
|
24
25
|
import java.util.Map;
|
|
25
26
|
import java.util.List;
|
|
26
27
|
|
|
27
28
|
import javax.annotation.Nullable;
|
|
28
29
|
|
|
30
|
+
import io.reactivex.Flowable;
|
|
29
31
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
30
32
|
import io.reactivex.disposables.Disposable;
|
|
31
33
|
import io.reactivex.schedulers.Schedulers;
|
|
32
34
|
|
|
33
35
|
public class DnssdImpl implements Zeroconf {
|
|
36
|
+
private static final String TAG = "DnssdImpl";
|
|
37
|
+
|
|
34
38
|
private Rx2Dnssd rxDnssd;
|
|
35
39
|
|
|
36
40
|
@Nullable
|
|
@@ -74,26 +78,49 @@ public class DnssdImpl implements Zeroconf {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
String serviceType = getServiceType(type, protocol);
|
|
77
|
-
Log.d(
|
|
81
|
+
Log.d(TAG, "Starting DNSSD scan for: " + serviceType);
|
|
78
82
|
|
|
79
83
|
// Emit start event
|
|
80
84
|
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_START, null);
|
|
81
85
|
|
|
82
86
|
browseDisposable = rxDnssd.browse(serviceType, "local.")
|
|
83
|
-
.
|
|
84
|
-
|
|
87
|
+
.doOnNext(bonjourService -> {
|
|
88
|
+
if (!bonjourService.isLost()) {
|
|
89
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_FOUND, serviceNameToMap(bonjourService));
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
// Resolve each service independently so a single failure doesn't end the whole browse
|
|
93
|
+
.flatMap(bonjourService -> bonjourService.isLost()
|
|
94
|
+
? Flowable.just(bonjourService)
|
|
95
|
+
: Flowable.just(bonjourService)
|
|
96
|
+
.compose(rxDnssd.resolve())
|
|
97
|
+
.compose(rxDnssd.queryRecords())
|
|
98
|
+
.onErrorResumeNext((Throwable throwable) -> {
|
|
99
|
+
Log.e(TAG, "Error resolving service: ", throwable);
|
|
100
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Resolving service failed: " + throwable.getMessage());
|
|
101
|
+
return Flowable.empty();
|
|
102
|
+
}))
|
|
85
103
|
.subscribeOn(Schedulers.io())
|
|
86
104
|
.observeOn(AndroidSchedulers.mainThread())
|
|
87
105
|
.subscribe(bonjourService -> {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
if (bonjourService.isLost()) {
|
|
107
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_REMOVE, serviceNameToMap(bonjourService));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_RESOLVE, serviceInfoToMap(bonjourService));
|
|
91
111
|
}, throwable -> {
|
|
92
|
-
Log.e(
|
|
93
|
-
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, throwable.getMessage());
|
|
112
|
+
Log.e(TAG, "Error browsing services: ", throwable);
|
|
113
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Browsing services failed: " + throwable.getMessage());
|
|
114
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_STOP, null);
|
|
94
115
|
});
|
|
95
116
|
}
|
|
96
117
|
|
|
118
|
+
private WritableMap serviceNameToMap(BonjourService serviceInfo) {
|
|
119
|
+
WritableMap service = new WritableNativeMap();
|
|
120
|
+
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
121
|
+
return service;
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
private String getServiceType(String type, String protocol) {
|
|
98
125
|
return String.format("_%s._%s", type, protocol);
|
|
99
126
|
}
|
|
@@ -102,10 +129,11 @@ public class DnssdImpl implements Zeroconf {
|
|
|
102
129
|
WritableMap service = new WritableNativeMap();
|
|
103
130
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
104
131
|
final List<InetAddress> hostList = serviceInfo.getInetAddresses();
|
|
105
|
-
final String
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
final String hostname = serviceInfo.getHostname();
|
|
133
|
+
final String fullServiceName = hostname != null
|
|
134
|
+
? hostname + serviceInfo.getRegType()
|
|
135
|
+
: serviceInfo.getServiceName();
|
|
136
|
+
service.putString(ZeroconfModule.KEY_SERVICE_HOST, hostname != null ? hostname : serviceInfo.getServiceName());
|
|
109
137
|
|
|
110
138
|
WritableArray addresses = new WritableNativeArray();
|
|
111
139
|
for (InetAddress host : hostList) {
|
|
@@ -160,7 +188,7 @@ public class DnssdImpl implements Zeroconf {
|
|
|
160
188
|
}
|
|
161
189
|
|
|
162
190
|
@Override
|
|
163
|
-
public void registerService(String type, String protocol, String domain, String name, int port,
|
|
191
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt) {
|
|
164
192
|
BonjourService bs = new BonjourService.Builder(0, 0, name, getServiceType(type, protocol), null)
|
|
165
193
|
.port(port)
|
|
166
194
|
.dnsRecords(getTxtRecordMap(txt))
|
|
@@ -170,23 +198,32 @@ public class DnssdImpl implements Zeroconf {
|
|
|
170
198
|
.subscribeOn(Schedulers.io())
|
|
171
199
|
.observeOn(AndroidSchedulers.mainThread())
|
|
172
200
|
.subscribe(bonjourService -> {
|
|
173
|
-
Log.i(
|
|
201
|
+
Log.i(TAG, "Registered service " + bonjourService.toString());
|
|
174
202
|
|
|
175
203
|
mPublishedServices.put(bs.getServiceName(), bs);
|
|
176
204
|
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_PUBLISHED, serviceInfoToMap(bonjourService));
|
|
177
205
|
}, throwable -> {
|
|
178
|
-
Log.e(
|
|
206
|
+
Log.e(TAG, "Error registering service: ", throwable);
|
|
207
|
+
zeroconfModule.sendEvent(reactApplicationContext, ZeroconfModule.EVENT_ERROR, "Registering service failed: " + throwable.getMessage());
|
|
208
|
+
mRegisteredDisposables.remove(name);
|
|
179
209
|
});
|
|
180
210
|
|
|
181
211
|
mRegisteredDisposables.put(name, registerDisposable);
|
|
182
212
|
}
|
|
183
213
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
214
|
+
@Override
|
|
215
|
+
public void unregisterAllServices() {
|
|
216
|
+
for (String serviceName : new ArrayList<>(mRegisteredDisposables.keySet())) {
|
|
217
|
+
unregisterService(serviceName);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private Map<String, String> getTxtRecordMap(ReadableArray txt) {
|
|
222
|
+
// LinkedHashMap keeps the TXT record order
|
|
223
|
+
Map<String, String> txtMap = new LinkedHashMap<>();
|
|
224
|
+
for (int i = 0; i < txt.size(); i++) {
|
|
225
|
+
ReadableArray pair = txt.getArray(i);
|
|
226
|
+
txtMap.put(pair.getString(0), pair.getString(1));
|
|
190
227
|
}
|
|
191
228
|
return txtMap;
|
|
192
229
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,40 @@ const ImplType = exports.ImplType = {
|
|
|
11
11
|
NSD: 'NSD',
|
|
12
12
|
DNSSD: 'DNSSD'
|
|
13
13
|
};
|
|
14
|
+
const isIPv6 = address => address.includes(':');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sort addresses IPv4 first and expose them by family
|
|
18
|
+
*/
|
|
19
|
+
const withAddressFamilies = service => {
|
|
20
|
+
const addresses = Array.isArray(service.addresses) ? service.addresses : [];
|
|
21
|
+
const ipv4 = addresses.filter(address => !isIPv6(address));
|
|
22
|
+
const ipv6 = addresses.filter(isIPv6);
|
|
23
|
+
return {
|
|
24
|
+
...service,
|
|
25
|
+
addresses: [...ipv4, ...ipv6],
|
|
26
|
+
ipv4,
|
|
27
|
+
ipv6
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* TXT records as ordered [key, value] pairs, from an object or an array of pairs
|
|
33
|
+
*/
|
|
34
|
+
const toTxtPairs = txt => {
|
|
35
|
+
const entries = Array.isArray(txt) ? txt : Object.entries(txt || {});
|
|
36
|
+
return entries.map(([key, value]) => [String(key), String(value)]);
|
|
37
|
+
};
|
|
14
38
|
class Zeroconf extends _events.EventEmitter {
|
|
15
39
|
constructor(props) {
|
|
16
40
|
super(props);
|
|
41
|
+
if (!RNZeroconf) {
|
|
42
|
+
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.');
|
|
43
|
+
}
|
|
17
44
|
this._services = {};
|
|
18
45
|
this._publishedServices = {};
|
|
46
|
+
this._publishedImplTypes = {};
|
|
47
|
+
this._scanImplType = null;
|
|
19
48
|
this._dListeners = {};
|
|
20
49
|
this.addDeviceListeners();
|
|
21
50
|
}
|
|
@@ -56,10 +85,11 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
56
85
|
this.emit('remove', name);
|
|
57
86
|
this.emit('update');
|
|
58
87
|
});
|
|
59
|
-
this._dListeners.resolved = _reactNative.DeviceEventEmitter.addListener('RNZeroconfResolved',
|
|
60
|
-
if (!
|
|
88
|
+
this._dListeners.resolved = _reactNative.DeviceEventEmitter.addListener('RNZeroconfResolved', data => {
|
|
89
|
+
if (!data || !data.name) {
|
|
61
90
|
return;
|
|
62
91
|
}
|
|
92
|
+
const service = withAddressFamilies(data);
|
|
63
93
|
this._services[service.name] = service;
|
|
64
94
|
this.emit('resolved', service);
|
|
65
95
|
this.emit('update');
|
|
@@ -103,6 +133,11 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
103
133
|
this._services = {};
|
|
104
134
|
this.emit('update');
|
|
105
135
|
if (_reactNative.Platform.OS === 'android') {
|
|
136
|
+
if (this._scanImplType && implType !== this._scanImplType) {
|
|
137
|
+
// Only one scan runs at a time, stop the one running on the other implementation
|
|
138
|
+
RNZeroconf.stop(this._scanImplType);
|
|
139
|
+
}
|
|
140
|
+
this._scanImplType = implType;
|
|
106
141
|
RNZeroconf.scan(type, protocol, domain, implType);
|
|
107
142
|
} else {
|
|
108
143
|
RNZeroconf.scan(type, protocol, domain);
|
|
@@ -110,9 +145,10 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
110
145
|
}
|
|
111
146
|
|
|
112
147
|
/**
|
|
113
|
-
* Stop current scan if any
|
|
148
|
+
* Stop current scan if any,
|
|
149
|
+
* Defaults to the implementation used by the last scan
|
|
114
150
|
*/
|
|
115
|
-
stop(implType = ImplType.NSD) {
|
|
151
|
+
stop(implType = this._scanImplType || ImplType.NSD) {
|
|
116
152
|
if (_reactNative.Platform.OS === 'android') {
|
|
117
153
|
RNZeroconf.stop(implType);
|
|
118
154
|
} else {
|
|
@@ -124,21 +160,22 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
124
160
|
* Publish a service
|
|
125
161
|
*/
|
|
126
162
|
publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
|
|
127
|
-
|
|
128
|
-
Object.entries(txt).map(([key, value]) => txt[key] = value.toString());
|
|
129
|
-
}
|
|
163
|
+
const txtRecord = toTxtPairs(txt);
|
|
130
164
|
if (_reactNative.Platform.OS === 'android') {
|
|
131
|
-
|
|
165
|
+
this._publishedImplTypes[name] = implType;
|
|
166
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType);
|
|
132
167
|
} else {
|
|
133
|
-
RNZeroconf.registerService(type, protocol, domain, name, port,
|
|
168
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord);
|
|
134
169
|
}
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
/**
|
|
138
|
-
* Unpublish a service
|
|
173
|
+
* Unpublish a service,
|
|
174
|
+
* Defaults to the implementation the service was published with
|
|
139
175
|
*/
|
|
140
|
-
unpublishService(name, implType = ImplType.NSD) {
|
|
176
|
+
unpublishService(name, implType = this._publishedImplTypes[name] || ImplType.NSD) {
|
|
141
177
|
if (_reactNative.Platform.OS === 'android') {
|
|
178
|
+
delete this._publishedImplTypes[name];
|
|
142
179
|
RNZeroconf.unregisterService(name, implType);
|
|
143
180
|
} else {
|
|
144
181
|
RNZeroconf.unregisterService(name);
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
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 IPv6 addresses, IPv4 first */
|
|
18
|
+
addresses: string[]
|
|
19
|
+
ipv4: string[]
|
|
20
|
+
ipv6: string[]
|
|
21
|
+
/** TXT record attributes */
|
|
22
|
+
txt: Record<string, string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type TxtValue = string | number | boolean
|
|
26
|
+
|
|
27
|
+
/** TXT records as an object, or as [key, value] pairs to control the order */
|
|
28
|
+
export type TxtRecord = Record<string, TxtValue> | Array<[string, TxtValue]>
|
|
29
|
+
|
|
30
|
+
/** Published services don't carry the address family fields */
|
|
31
|
+
export type PublishedService = Omit<Service, 'ipv4' | 'ipv6'>
|
|
32
|
+
|
|
33
|
+
export interface ZeroconfEvents {
|
|
34
|
+
start: () => void
|
|
35
|
+
stop: () => void
|
|
36
|
+
/** Service found, before resolution */
|
|
37
|
+
found: (name: string) => void
|
|
38
|
+
/** Service fully resolved with network info */
|
|
39
|
+
resolved: (service: Service) => void
|
|
40
|
+
/** Service removed from the network */
|
|
41
|
+
remove: (name: string) => void
|
|
42
|
+
/** Services list changed */
|
|
43
|
+
update: () => void
|
|
44
|
+
error: (error: Error) => void
|
|
45
|
+
published: (service: PublishedService) => void
|
|
46
|
+
unpublished: (service: PublishedService) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Extends the `events` package EventEmitter */
|
|
50
|
+
export default class Zeroconf {
|
|
51
|
+
constructor()
|
|
52
|
+
|
|
53
|
+
on<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
54
|
+
once<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
55
|
+
addListener<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
56
|
+
removeListener<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
57
|
+
off<E extends keyof ZeroconfEvents>(event: E, listener: ZeroconfEvents[E]): this
|
|
58
|
+
removeAllListeners(event?: keyof ZeroconfEvents): this
|
|
59
|
+
listenerCount(event: keyof ZeroconfEvents): number
|
|
60
|
+
|
|
61
|
+
/** Add the native event listeners (called automatically in the constructor) */
|
|
62
|
+
addDeviceListeners(): void
|
|
63
|
+
|
|
64
|
+
/** Remove the native event listeners, call it when you are done with the instance */
|
|
65
|
+
removeDeviceListeners(): void
|
|
66
|
+
|
|
67
|
+
/** All the services found so far, keyed by name */
|
|
68
|
+
getServices(): Record<string, Service>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Scan for services, defaults to `_http._tcp.` on the `local.` domain
|
|
72
|
+
* @param implType Android only, defaults to `NSD`
|
|
73
|
+
*/
|
|
74
|
+
scan(type?: string, protocol?: string, domain?: string, implType?: ImplType): void
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stop the current scan
|
|
78
|
+
* @param implType Android only, defaults to the implementation used by the last scan
|
|
79
|
+
*/
|
|
80
|
+
stop(implType?: ImplType): void
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Publish a service. Type and protocol are passed without underscores, e.g. `('http', 'tcp', ...)`
|
|
84
|
+
* @param implType Android only, defaults to `NSD`
|
|
85
|
+
*/
|
|
86
|
+
publishService(
|
|
87
|
+
type: string,
|
|
88
|
+
protocol: string,
|
|
89
|
+
domain: string | undefined,
|
|
90
|
+
name: string,
|
|
91
|
+
port: number,
|
|
92
|
+
txt?: TxtRecord,
|
|
93
|
+
implType?: ImplType,
|
|
94
|
+
): void
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Unpublish a service
|
|
98
|
+
* @param implType Android only, defaults to the implementation the service was published with
|
|
99
|
+
*/
|
|
100
|
+
unpublishService(name: string, implType?: ImplType): void
|
|
101
|
+
}
|
|
@@ -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);
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
#import <React/RCTBridge.h>
|
|
11
11
|
#import <React/RCTBridgeModule.h>
|
|
12
12
|
#import <React/RCTEventDispatcher.h>
|
|
13
|
+
#import <React/RCTInvalidating.h>
|
|
13
14
|
|
|
14
|
-
@interface RNZeroconf : NSObject <RCTBridgeModule, NSNetServiceBrowserDelegate, NSNetServiceDelegate>
|
|
15
|
+
@interface RNZeroconf : NSObject <RCTBridgeModule, RCTInvalidating, NSNetServiceBrowserDelegate, NSNetServiceDelegate>
|
|
15
16
|
|
|
16
17
|
@property (nonatomic, strong) NSNetServiceBrowser *browser;
|
|
17
18
|
|
|
@@ -49,15 +49,14 @@ RCT_EXPORT_METHOD(registerService:(NSString *)type
|
|
|
49
49
|
domain:(NSString *)domain
|
|
50
50
|
name:(NSString *)name
|
|
51
51
|
port:(int)port
|
|
52
|
-
txt:(
|
|
52
|
+
txt:(NSArray<NSArray<NSString *> *> *)txt)
|
|
53
53
|
{
|
|
54
54
|
const NSNetService *svc = [[NSNetService alloc] initWithDomain:domain type:[NSString stringWithFormat:@"_%@._%@.", type, protocol] name:name port:port];
|
|
55
55
|
[svc setDelegate:self];
|
|
56
56
|
[svc scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
|
57
57
|
|
|
58
|
-
if (txt) {
|
|
59
|
-
|
|
60
|
-
[svc setTXTRecordData:txtData];
|
|
58
|
+
if (txt.count > 0) {
|
|
59
|
+
[svc setTXTRecordData:[self TXTRecordDataFromPairs:txt]];
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
[svc publish];
|
|
@@ -106,6 +105,14 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
106
105
|
return;
|
|
107
106
|
}
|
|
108
107
|
|
|
108
|
+
// Stop any pending resolve for the removed service
|
|
109
|
+
NSNetService *resolving = self.resolvingServices[service.name];
|
|
110
|
+
if (resolving) {
|
|
111
|
+
resolving.delegate = nil;
|
|
112
|
+
[resolving stop];
|
|
113
|
+
[self.resolvingServices removeObjectForKey:service.name];
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
NSDictionary *serviceInfo = [RNNetServiceSerializer serializeServiceToDictionary:service resolved:NO];
|
|
110
117
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfRemove" body:serviceInfo];
|
|
111
118
|
}
|
|
@@ -205,6 +212,42 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
205
212
|
return self;
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
// Builds the TXT record in the given order, unlike dataFromTXTRecordDictionary
|
|
216
|
+
- (NSData *) TXTRecordDataFromPairs:(NSArray<NSArray<NSString *> *> *)pairs
|
|
217
|
+
{
|
|
218
|
+
NSMutableData *data = [NSMutableData data];
|
|
219
|
+
for (NSArray<NSString *> *pair in pairs) {
|
|
220
|
+
if (pair.count != 2) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
NSData *entry = [[NSString stringWithFormat:@"%@=%@", pair[0], pair[1]] dataUsingEncoding:NSUTF8StringEncoding];
|
|
224
|
+
if (entry.length > 255) {
|
|
225
|
+
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfError" body:[NSString stringWithFormat:@"TXT record entry %@ is longer than 255 bytes", pair[0]]];
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
uint8_t length = (uint8_t)entry.length;
|
|
229
|
+
[data appendBytes:&length length:1];
|
|
230
|
+
[data appendData:entry];
|
|
231
|
+
}
|
|
232
|
+
return data;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Called when the bridge is torn down (e.g. reload): stop scanning and unpublish services
|
|
236
|
+
- (void) invalidate
|
|
237
|
+
{
|
|
238
|
+
[self.browser stop];
|
|
239
|
+
for (NSNetService *service in self.resolvingServices.allValues) {
|
|
240
|
+
service.delegate = nil;
|
|
241
|
+
[service stop];
|
|
242
|
+
}
|
|
243
|
+
[self.resolvingServices removeAllObjects];
|
|
244
|
+
for (NSNetService *service in self.publishedServices.allValues) {
|
|
245
|
+
service.delegate = nil;
|
|
246
|
+
[service stop];
|
|
247
|
+
}
|
|
248
|
+
[self.publishedServices removeAllObjects];
|
|
249
|
+
}
|
|
250
|
+
|
|
208
251
|
- (void) reportError:(NSDictionary *)errorDict
|
|
209
252
|
{
|
|
210
253
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfError" body:[NSString stringWithFormat:@"%@",errorDict]];
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-zeroconf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
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
|
@@ -8,12 +8,40 @@ export const ImplType = {
|
|
|
8
8
|
DNSSD: 'DNSSD',
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const isIPv6 = address => address.includes(':')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sort addresses IPv4 first and expose them by family
|
|
15
|
+
*/
|
|
16
|
+
const withAddressFamilies = service => {
|
|
17
|
+
const addresses = Array.isArray(service.addresses) ? service.addresses : []
|
|
18
|
+
const ipv4 = addresses.filter(address => !isIPv6(address))
|
|
19
|
+
const ipv6 = addresses.filter(isIPv6)
|
|
20
|
+
return { ...service, addresses: [...ipv4, ...ipv6], ipv4, ipv6 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* TXT records as ordered [key, value] pairs, from an object or an array of pairs
|
|
25
|
+
*/
|
|
26
|
+
const toTxtPairs = txt => {
|
|
27
|
+
const entries = Array.isArray(txt) ? txt : Object.entries(txt || {})
|
|
28
|
+
return entries.map(([key, value]) => [String(key), String(value)])
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export default class Zeroconf extends EventEmitter {
|
|
12
32
|
constructor(props) {
|
|
13
33
|
super(props)
|
|
14
34
|
|
|
35
|
+
if (!RNZeroconf) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'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.',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
this._services = {}
|
|
16
42
|
this._publishedServices = {}
|
|
43
|
+
this._publishedImplTypes = {}
|
|
44
|
+
this._scanImplType = null
|
|
17
45
|
this._dListeners = {}
|
|
18
46
|
|
|
19
47
|
this.addDeviceListeners()
|
|
@@ -64,11 +92,12 @@ export default class Zeroconf extends EventEmitter {
|
|
|
64
92
|
this.emit('update')
|
|
65
93
|
})
|
|
66
94
|
|
|
67
|
-
this._dListeners.resolved = DeviceEventEmitter.addListener('RNZeroconfResolved',
|
|
68
|
-
if (!
|
|
95
|
+
this._dListeners.resolved = DeviceEventEmitter.addListener('RNZeroconfResolved', data => {
|
|
96
|
+
if (!data || !data.name) {
|
|
69
97
|
return
|
|
70
98
|
}
|
|
71
99
|
|
|
100
|
+
const service = withAddressFamilies(data)
|
|
72
101
|
this._services[service.name] = service
|
|
73
102
|
this.emit('resolved', service)
|
|
74
103
|
this.emit('update')
|
|
@@ -122,6 +151,11 @@ export default class Zeroconf extends EventEmitter {
|
|
|
122
151
|
this._services = {}
|
|
123
152
|
this.emit('update')
|
|
124
153
|
if (Platform.OS === 'android') {
|
|
154
|
+
if (this._scanImplType && implType !== this._scanImplType) {
|
|
155
|
+
// Only one scan runs at a time, stop the one running on the other implementation
|
|
156
|
+
RNZeroconf.stop(this._scanImplType)
|
|
157
|
+
}
|
|
158
|
+
this._scanImplType = implType
|
|
125
159
|
RNZeroconf.scan(type, protocol, domain, implType)
|
|
126
160
|
} else {
|
|
127
161
|
RNZeroconf.scan(type, protocol, domain)
|
|
@@ -129,9 +163,10 @@ export default class Zeroconf extends EventEmitter {
|
|
|
129
163
|
}
|
|
130
164
|
|
|
131
165
|
/**
|
|
132
|
-
* Stop current scan if any
|
|
166
|
+
* Stop current scan if any,
|
|
167
|
+
* Defaults to the implementation used by the last scan
|
|
133
168
|
*/
|
|
134
|
-
stop(implType = ImplType.NSD) {
|
|
169
|
+
stop(implType = this._scanImplType || ImplType.NSD) {
|
|
135
170
|
if (Platform.OS === 'android') {
|
|
136
171
|
RNZeroconf.stop(implType)
|
|
137
172
|
} else {
|
|
@@ -143,21 +178,22 @@ export default class Zeroconf extends EventEmitter {
|
|
|
143
178
|
* Publish a service
|
|
144
179
|
*/
|
|
145
180
|
publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
|
|
146
|
-
|
|
147
|
-
Object.entries(txt).map(([key, value]) => (txt[key] = value.toString()))
|
|
148
|
-
}
|
|
181
|
+
const txtRecord = toTxtPairs(txt)
|
|
149
182
|
if (Platform.OS === 'android') {
|
|
150
|
-
|
|
183
|
+
this._publishedImplTypes[name] = implType
|
|
184
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType)
|
|
151
185
|
} else {
|
|
152
|
-
RNZeroconf.registerService(type, protocol, domain, name, port,
|
|
186
|
+
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord)
|
|
153
187
|
}
|
|
154
188
|
}
|
|
155
189
|
|
|
156
190
|
/**
|
|
157
|
-
* Unpublish a service
|
|
191
|
+
* Unpublish a service,
|
|
192
|
+
* Defaults to the implementation the service was published with
|
|
158
193
|
*/
|
|
159
|
-
unpublishService(name, implType = ImplType.NSD) {
|
|
194
|
+
unpublishService(name, implType = this._publishedImplTypes[name] || ImplType.NSD) {
|
|
160
195
|
if (Platform.OS === 'android') {
|
|
196
|
+
delete this._publishedImplTypes[name]
|
|
161
197
|
RNZeroconf.unregisterService(name, implType)
|
|
162
198
|
} else {
|
|
163
199
|
RNZeroconf.unregisterService(name)
|