@devioarts/capacitor-mdns 0.0.3 → 0.1.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 +54 -67
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +134 -37
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +18 -5
- package/dist/docs.json +65 -3
- package/dist/esm/definitions.d.ts +27 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +3 -1
- package/dist/esm/web.js +12 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +13 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +13 -3
- package/dist/plugin.js.map +1 -1
- package/docs/electron.md +65 -0
- package/docs/platform-setup.md +52 -0
- package/docs/playground.md +19 -0
- package/docs/runtime-behavior.md +14 -0
- package/electron/dist/plugin-settings.js +15 -0
- package/electron/dist/plugin-settings.js.map +1 -0
- package/electron/dist/plugin.cjs.js +344 -0
- package/electron/dist/plugin.cjs.js.map +1 -0
- package/electron/global.d.ts +2 -0
- package/electron/mdns-bridge.cjs +8 -2
- package/electron/mdns.ts +151 -63
- package/electron/rollup.config.mjs +22 -0
- package/electron/src/index.ts +1 -0
- package/electron/src/plugin-settings.ts +12 -0
- package/electron/tsconfig.json +21 -0
- package/electron/types-public/index.d.ts +25 -0
- package/ios/Sources/mDNSPlugin/mDNS.swift +95 -28
- package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +12 -3
- package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +3 -9
- package/package.json +46 -18
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# @devioarts/capacitor-mdns
|
|
2
2
|
|
|
3
|
-
mDNS plugin for Capacitor that supports Bonjour/mDNS advertisements and discovery
|
|
3
|
+
mDNS plugin for Capacitor that supports Bonjour/mDNS advertisements and discovery on iOS,
|
|
4
|
+
Android, Electron, and a safe web fallback.
|
|
4
5
|
|
|
5
|
-
#### Supported platforms: ✓ iOS ✓ Android ✓ Electron
|
|
6
|
-
|
|
6
|
+
#### Supported platforms: ✓ iOS ✓ Android ✓ Electron ✓ Web fallback
|
|
7
|
+
|
|
8
|
+
The in-repository [`playground`](./playground) is the example app used during development.
|
|
7
9
|
|
|
8
10
|
## Install
|
|
9
11
|
|
|
@@ -12,82 +14,37 @@ npm install @devioarts/capacitor-mdns
|
|
|
12
14
|
npx cap sync
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
##
|
|
16
|
-
#### /android/app/src/main/AndroidManifest.xml
|
|
17
|
-
```xml
|
|
18
|
-
<uses-permission android:name="android.permission.INTERNET" />
|
|
19
|
-
```
|
|
17
|
+
## Usage
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```xml
|
|
24
|
-
<key>NSLocalNetworkUsageDescription</key>
|
|
25
|
-
<string>It is needed for the correct functioning of the application</string>
|
|
26
|
-
<key>NSAppTransportSecurity</key>
|
|
27
|
-
<dict>
|
|
28
|
-
<key>NSAllowsLocalNetworking</key>
|
|
29
|
-
<true/>
|
|
30
|
-
</dict>
|
|
31
|
-
<key>NSBonjourServices</key>
|
|
32
|
-
<array>
|
|
33
|
-
<string>_http._tcp</string>
|
|
34
|
-
</array>
|
|
35
|
-
```
|
|
19
|
+
```ts
|
|
20
|
+
import { mDNS } from '@devioarts/capacitor-mdns';
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
## ElectronJS
|
|
22
|
+
const runtime = await mDNS.getPluginPlatform();
|
|
39
23
|
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
await mDNS.startBroadcast({ type: '_http._tcp.', name: 'MyService', port: 9100 });
|
|
25
|
+
const result = await mDNS.discover({ type: '_http._tcp.', timeout: 3000 });
|
|
26
|
+
await mDNS.stopBroadcast();
|
|
42
27
|
```
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
> base, if you run electron differently, you may need to adjust the code.
|
|
29
|
+
## Platform Setup
|
|
46
30
|
|
|
47
|
-
|
|
31
|
+
Android requires mDNS/network permissions in `AndroidManifest.xml`; the plugin also declares them
|
|
32
|
+
so they are merged during sync. iOS requires local network keys in `Info.plist`.
|
|
33
|
+
Electron support is intended for [`devioarts/capacitor-electron`](https://github.com/devioarts/capacitor-electron)
|
|
34
|
+
and uses metadata exported from `@devioarts/capacitor-mdns/electron/settings`.
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
app.whenReady().then(() => {
|
|
56
|
-
//...
|
|
57
|
-
mdns.init();
|
|
58
|
-
//...
|
|
59
|
-
});
|
|
60
|
-
/* Or you can use app.on:ready (whenReady is recomended)
|
|
61
|
-
app.on('ready', () => {
|
|
62
|
-
// ...
|
|
63
|
-
mdns.init();
|
|
64
|
-
// ...
|
|
65
|
-
});
|
|
66
|
-
*/
|
|
67
|
-
|
|
68
|
-
app.on('before-quit', async () => {
|
|
69
|
-
// ...
|
|
70
|
-
mdns.destroy();
|
|
71
|
-
// ...
|
|
72
|
-
})
|
|
73
|
-
//...
|
|
74
|
-
```
|
|
75
|
-
### electron/preload.cjs
|
|
76
|
-
```javascript
|
|
77
|
-
//...
|
|
78
|
-
// THIS IS IMPORTANT FOR PLUGIN!
|
|
79
|
-
const {createMDNSAPI} = require("@devioarts/capacitor-mdns/electron/mdns-bridge.cjs");
|
|
80
|
-
//...
|
|
81
|
-
// THIS IS IMPORTANT FOR PLUGIN!
|
|
82
|
-
contextBridge.exposeInMainWorld('mDNS', createMDNSAPI({ ipcRenderer }));
|
|
83
|
-
contextBridge.exposeInMainWorld('mdns', createMDNSAPI({ ipcRenderer })) // alias
|
|
84
|
-
```
|
|
36
|
+
Detailed setup lives in:
|
|
37
|
+
|
|
38
|
+
- [iOS and Android setup](./docs/platform-setup.md)
|
|
39
|
+
- [Electron setup](./docs/electron.md)
|
|
40
|
+
- [Playground](./docs/playground.md)
|
|
41
|
+
- [Runtime behavior](./docs/runtime-behavior.md)
|
|
85
42
|
|
|
86
|
-
---
|
|
87
43
|
## API
|
|
88
44
|
|
|
89
45
|
<docgen-index>
|
|
90
46
|
|
|
47
|
+
* [`getPluginPlatform()`](#getpluginplatform)
|
|
91
48
|
* [`startBroadcast(...)`](#startbroadcast)
|
|
92
49
|
* [`stopBroadcast()`](#stopbroadcast)
|
|
93
50
|
* [`discover(...)`](#discover)
|
|
@@ -101,6 +58,19 @@ contextBridge.exposeInMainWorld('mdns', createMDNSAPI({ ipcRenderer })) // alias
|
|
|
101
58
|
|
|
102
59
|
Public API surface of the Capacitor mDNS plugin.
|
|
103
60
|
|
|
61
|
+
### getPluginPlatform()
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
getPluginPlatform() => Promise<MdnsPluginPlatformResult>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Return the platform implementation currently serving plugin calls.
|
|
68
|
+
|
|
69
|
+
**Returns:** <code>Promise<<a href="#mdnspluginplatformresult">MdnsPluginPlatformResult</a>></code>
|
|
70
|
+
|
|
71
|
+
--------------------
|
|
72
|
+
|
|
73
|
+
|
|
104
74
|
### startBroadcast(...)
|
|
105
75
|
|
|
106
76
|
```typescript
|
|
@@ -151,6 +121,16 @@ Discover services of a given type and optionally filter by instance name.
|
|
|
151
121
|
### Interfaces
|
|
152
122
|
|
|
153
123
|
|
|
124
|
+
#### MdnsPluginPlatformResult
|
|
125
|
+
|
|
126
|
+
Result of getPluginPlatform(). Useful for validating that calls are routed
|
|
127
|
+
through the expected platform bridge.
|
|
128
|
+
|
|
129
|
+
| Prop | Type | Description |
|
|
130
|
+
| -------------- | ----------------------------------------------------------------- | -------------------------------------------------- |
|
|
131
|
+
| **`platform`** | <code><a href="#mdnspluginplatform">MdnsPluginPlatform</a></code> | Platform implementation that produced this result. |
|
|
132
|
+
|
|
133
|
+
|
|
154
134
|
#### MdnsBroadcastResult
|
|
155
135
|
|
|
156
136
|
Result of startBroadcast(). Indicates whether advertising is active
|
|
@@ -231,6 +211,13 @@ Options for Bonjour/mDNS discovery.
|
|
|
231
211
|
### Type Aliases
|
|
232
212
|
|
|
233
213
|
|
|
214
|
+
#### MdnsPluginPlatform
|
|
215
|
+
|
|
216
|
+
Runtime implementation that handled a plugin call.
|
|
217
|
+
|
|
218
|
+
<code>'ios' | 'android' | 'electron' | 'web'</code>
|
|
219
|
+
|
|
220
|
+
|
|
234
221
|
#### MdnsTxt
|
|
235
222
|
|
|
236
223
|
Key–value map for TXT records of a Bonjour/mDNS service.
|
|
@@ -8,6 +8,8 @@ import android.os.Build
|
|
|
8
8
|
import android.os.Handler
|
|
9
9
|
import android.os.Looper
|
|
10
10
|
import kotlinx.coroutines.*
|
|
11
|
+
import kotlinx.coroutines.sync.Mutex
|
|
12
|
+
import kotlinx.coroutines.sync.withLock
|
|
11
13
|
import java.util.concurrent.Executor
|
|
12
14
|
import java.util.concurrent.atomic.AtomicReference
|
|
13
15
|
|
|
@@ -31,6 +33,10 @@ class mDNS(
|
|
|
31
33
|
context: Context,
|
|
32
34
|
private val externalScope: CoroutineScope? = null
|
|
33
35
|
) {
|
|
36
|
+
private companion object {
|
|
37
|
+
const val PUBLISH_TIMEOUT_MS = 5000L
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
/** Normalized service representation returned to the bridge. */
|
|
35
41
|
data class MdnsService(
|
|
36
42
|
val name: String,
|
|
@@ -39,16 +45,26 @@ class mDNS(
|
|
|
39
45
|
val port: Int
|
|
40
46
|
)
|
|
41
47
|
|
|
48
|
+
private class RegistrationSession(
|
|
49
|
+
val listener: NsdManager.RegistrationListener,
|
|
50
|
+
val timeoutJob: Job,
|
|
51
|
+
val onSuccess: (String) -> Unit,
|
|
52
|
+
val onError: (Throwable) -> Unit
|
|
53
|
+
) {
|
|
54
|
+
var completed = false
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
/** NSD system service. */
|
|
43
58
|
private val nsd: NsdManager =
|
|
44
59
|
context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
|
45
60
|
|
|
46
61
|
/** Keep strong references to listeners so they outlive registration/discovery. */
|
|
47
|
-
private
|
|
62
|
+
private var activeRegistration: RegistrationSession? = null
|
|
48
63
|
private val discListenerRef = AtomicReference<NsdManager.DiscoveryListener?>(null)
|
|
49
64
|
|
|
50
65
|
/** Optional external scope (from plugin); otherwise create our own on Main. */
|
|
51
66
|
private val scope = externalScope ?: CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
67
|
+
private val discoverMutex = Mutex()
|
|
52
68
|
|
|
53
69
|
/** Main looper utilities for non-suspending entry points. */
|
|
54
70
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
@@ -73,29 +89,58 @@ class mDNS(
|
|
|
73
89
|
onSuccess: (String) -> Unit,
|
|
74
90
|
onError: (Throwable) -> Unit
|
|
75
91
|
) {
|
|
76
|
-
require(port
|
|
92
|
+
require(port in 1..65535) { "Port must be between 1 and 65535" }
|
|
77
93
|
val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
|
|
78
94
|
|
|
79
95
|
runOnMain {
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
try {
|
|
97
|
+
// Restarting while publish is pending must complete the old JS promise.
|
|
98
|
+
stopActiveRegistration("Replaced by a new broadcast request")
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
val info = NsdServiceInfo().apply {
|
|
101
|
+
serviceType = type
|
|
102
|
+
serviceName = name
|
|
103
|
+
setPort(port)
|
|
104
|
+
}
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
lateinit var session: RegistrationSession
|
|
107
|
+
val listener = object : NsdManager.RegistrationListener {
|
|
108
|
+
override fun onServiceRegistered(nsi: NsdServiceInfo) =
|
|
109
|
+
completeRegistrationSuccess(session, nsi.serviceName)
|
|
110
|
+
|
|
111
|
+
override fun onRegistrationFailed(nsi: NsdServiceInfo, errorCode: Int) =
|
|
112
|
+
completeRegistrationError(
|
|
113
|
+
session,
|
|
114
|
+
IllegalStateException("Registration failed: $errorCode"),
|
|
115
|
+
clearActive = true
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
override fun onServiceUnregistered(nsi: NsdServiceInfo) { /* no-op */ }
|
|
119
|
+
override fun onUnregistrationFailed(nsi: NsdServiceInfo, errorCode: Int) { /* no-op */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
val timeoutJob = scope.launch {
|
|
123
|
+
delay(PUBLISH_TIMEOUT_MS)
|
|
124
|
+
runOnMain {
|
|
125
|
+
completeRegistrationError(
|
|
126
|
+
session,
|
|
127
|
+
IllegalStateException("Timed out waiting for service publish after ${PUBLISH_TIMEOUT_MS}ms"),
|
|
128
|
+
clearActive = true,
|
|
129
|
+
unregister = true
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
96
133
|
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
session = RegistrationSession(listener, timeoutJob, onSuccess, onError)
|
|
135
|
+
activeRegistration = session
|
|
136
|
+
try {
|
|
137
|
+
nsd.registerService(info, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
138
|
+
} catch (t: Throwable) {
|
|
139
|
+
completeRegistrationError(session, t, clearActive = true)
|
|
140
|
+
}
|
|
141
|
+
} catch (t: Throwable) {
|
|
142
|
+
onError(t)
|
|
143
|
+
}
|
|
99
144
|
}
|
|
100
145
|
}
|
|
101
146
|
|
|
@@ -104,7 +149,7 @@ class mDNS(
|
|
|
104
149
|
*/
|
|
105
150
|
fun stopBroadcast() {
|
|
106
151
|
runOnMain {
|
|
107
|
-
|
|
152
|
+
stopActiveRegistration("Publish stopped before completion")
|
|
108
153
|
}
|
|
109
154
|
}
|
|
110
155
|
|
|
@@ -127,7 +172,7 @@ class mDNS(
|
|
|
127
172
|
typeRaw: String,
|
|
128
173
|
targetName: String? = null,
|
|
129
174
|
timeoutMs: Int = 3000
|
|
130
|
-
): List<MdnsService> = withContext(Dispatchers.Main.immediate) {
|
|
175
|
+
): List<MdnsService> = discoverMutex.withLock { withContext(Dispatchers.Main.immediate) {
|
|
131
176
|
val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
|
|
132
177
|
|
|
133
178
|
// Prepare state
|
|
@@ -155,23 +200,35 @@ class mDNS(
|
|
|
155
200
|
fun completeWithCurrentResults(discoveryListener: NsdManager.DiscoveryListener) {
|
|
156
201
|
unregisterServiceInfoCallbacks()
|
|
157
202
|
safeStopDiscovery(discoveryListener)
|
|
203
|
+
discListenerRef.compareAndSet(discoveryListener, null)
|
|
158
204
|
if (!result.isCompleted) result.complete(found.toList())
|
|
159
205
|
}
|
|
206
|
+
fun completeWithError(discoveryListener: NsdManager.DiscoveryListener, error: Throwable) {
|
|
207
|
+
unregisterServiceInfoCallbacks()
|
|
208
|
+
safeStopDiscovery(discoveryListener)
|
|
209
|
+
discListenerRef.compareAndSet(discoveryListener, null)
|
|
210
|
+
if (!result.isCompleted) result.completeExceptionally(error)
|
|
211
|
+
}
|
|
160
212
|
|
|
161
213
|
@Suppress("DEPRECATION")
|
|
162
214
|
fun resolveLegacy(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
|
|
163
|
-
|
|
164
|
-
|
|
215
|
+
try {
|
|
216
|
+
nsd.resolveService(si, object : NsdManager.ResolveListener {
|
|
217
|
+
override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
|
|
165
218
|
|
|
166
|
-
|
|
167
|
-
|
|
219
|
+
override fun onServiceResolved(s: NsdServiceInfo) {
|
|
220
|
+
if (result.isCompleted) return
|
|
221
|
+
upsert(toMdnsService(s))
|
|
168
222
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
223
|
+
// Early-exit for exact/prefix target match.
|
|
224
|
+
if (targetName != null && matchesTarget(s.serviceName)) {
|
|
225
|
+
completeWithCurrentResults(discoveryListener)
|
|
226
|
+
}
|
|
172
227
|
}
|
|
173
|
-
}
|
|
174
|
-
})
|
|
228
|
+
})
|
|
229
|
+
} catch (_: Throwable) {
|
|
230
|
+
// Some Android releases throw when the NSD daemon is busy; ignore this candidate.
|
|
231
|
+
}
|
|
175
232
|
}
|
|
176
233
|
|
|
177
234
|
fun registerServiceInfoCallback(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
|
|
@@ -186,6 +243,7 @@ class mDNS(
|
|
|
186
243
|
override fun onServiceLost() { /* no-op */ }
|
|
187
244
|
|
|
188
245
|
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
|
246
|
+
if (result.isCompleted) return
|
|
189
247
|
upsert(toMdnsService(serviceInfo))
|
|
190
248
|
|
|
191
249
|
// Early-exit for exact/prefix target match.
|
|
@@ -205,14 +263,14 @@ class mDNS(
|
|
|
205
263
|
|
|
206
264
|
val listener = object : NsdManager.DiscoveryListener {
|
|
207
265
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
|
208
|
-
|
|
209
|
-
completeWithCurrentResults(this)
|
|
266
|
+
completeWithError(this, IllegalStateException("Discovery failed to start: $errorCode"))
|
|
210
267
|
}
|
|
211
268
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { /* no-op */ }
|
|
212
269
|
override fun onDiscoveryStarted(serviceType: String) { /* no-op */ }
|
|
213
270
|
override fun onDiscoveryStopped(serviceType: String) { /* no-op */ }
|
|
214
271
|
|
|
215
272
|
override fun onServiceFound(si: NsdServiceInfo) {
|
|
273
|
+
if (result.isCompleted) return
|
|
216
274
|
if (targetName != null && !matchesTarget(si.serviceName)) return
|
|
217
275
|
|
|
218
276
|
val discoveryListener = this
|
|
@@ -224,10 +282,15 @@ class mDNS(
|
|
|
224
282
|
|
|
225
283
|
// Start discovery and schedule timeout
|
|
226
284
|
discListenerRef.set(listener)
|
|
227
|
-
|
|
285
|
+
try {
|
|
286
|
+
nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
287
|
+
} catch (t: Throwable) {
|
|
288
|
+
discListenerRef.compareAndSet(listener, null)
|
|
289
|
+
throw t
|
|
290
|
+
}
|
|
228
291
|
|
|
229
292
|
val timeoutJob = scope.launch {
|
|
230
|
-
delay(timeoutMs.toLong())
|
|
293
|
+
delay(timeoutMs.coerceAtLeast(0).toLong())
|
|
231
294
|
completeWithCurrentResults(listener)
|
|
232
295
|
}
|
|
233
296
|
|
|
@@ -237,17 +300,19 @@ class mDNS(
|
|
|
237
300
|
// Cleanup in all cases (early-exit or timeout)
|
|
238
301
|
timeoutJob.cancel()
|
|
239
302
|
unregisterServiceInfoCallbacks()
|
|
240
|
-
discListenerRef.
|
|
303
|
+
if (discListenerRef.compareAndSet(listener, null)) safeStopDiscovery(listener)
|
|
241
304
|
}
|
|
242
|
-
}
|
|
305
|
+
} }
|
|
243
306
|
|
|
244
307
|
/**
|
|
245
308
|
* Close the manager and release any outstanding NSD listeners.
|
|
246
309
|
* Safe to call multiple times; also invoked by the plugin's handleOnDestroy().
|
|
247
310
|
*/
|
|
248
311
|
fun close() {
|
|
249
|
-
|
|
250
|
-
|
|
312
|
+
runOnMain {
|
|
313
|
+
stopActiveRegistration("mDNS manager closed before publish completed")
|
|
314
|
+
discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
|
|
315
|
+
}
|
|
251
316
|
if (externalScope == null) scope.cancel()
|
|
252
317
|
}
|
|
253
318
|
|
|
@@ -258,6 +323,38 @@ class mDNS(
|
|
|
258
323
|
try { nsd.unregisterService(l) } catch (_: Throwable) {}
|
|
259
324
|
}
|
|
260
325
|
|
|
326
|
+
private fun completeRegistrationSuccess(session: RegistrationSession, publishedName: String) {
|
|
327
|
+
if (session.completed) return
|
|
328
|
+
session.completed = true
|
|
329
|
+
session.timeoutJob.cancel()
|
|
330
|
+
session.onSuccess(publishedName)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private fun completeRegistrationError(
|
|
334
|
+
session: RegistrationSession,
|
|
335
|
+
error: Throwable,
|
|
336
|
+
clearActive: Boolean,
|
|
337
|
+
unregister: Boolean = false
|
|
338
|
+
) {
|
|
339
|
+
if (session.completed) return
|
|
340
|
+
session.completed = true
|
|
341
|
+
session.timeoutJob.cancel()
|
|
342
|
+
if (unregister) safeUnregister(session.listener)
|
|
343
|
+
if (clearActive && activeRegistration === session) activeRegistration = null
|
|
344
|
+
session.onError(error)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private fun stopActiveRegistration(reason: String) {
|
|
348
|
+
val session = activeRegistration ?: return
|
|
349
|
+
activeRegistration = null
|
|
350
|
+
safeUnregister(session.listener)
|
|
351
|
+
if (!session.completed) {
|
|
352
|
+
completeRegistrationError(session, IllegalStateException(reason), clearActive = false)
|
|
353
|
+
} else {
|
|
354
|
+
session.timeoutJob.cancel()
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
261
358
|
/** Best-effort stop discovery; NSD may throw if discovery is not active. */
|
|
262
359
|
private fun safeStopDiscovery(l: NsdManager.DiscoveryListener) {
|
|
263
360
|
try { nsd.stopServiceDiscovery(l) } catch (_: Throwable) {}
|
|
@@ -52,6 +52,14 @@ class mDNSPlugin : Plugin() {
|
|
|
52
52
|
|
|
53
53
|
private fun jnull(msg: String?): Any = msg ?: JSONObject.NULL
|
|
54
54
|
|
|
55
|
+
private fun normalizeType(raw: String?): String {
|
|
56
|
+
val type = raw?.trim()?.takeIf { it.isNotEmpty() } ?: "_http._tcp."
|
|
57
|
+
return if (type.endsWith(".")) type else "$type."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun normalizeName(raw: String?): String =
|
|
61
|
+
raw?.trim()?.takeIf { it.isNotEmpty() } ?: (context?.packageName ?: "DevIOArtsMDNS")
|
|
62
|
+
|
|
55
63
|
private fun jsResultBroadcast(publishing: Boolean, name: String, error: Boolean, msg: String?): JSObject =
|
|
56
64
|
JSObject().put("publishing", publishing)
|
|
57
65
|
.put("name", name)
|
|
@@ -72,6 +80,11 @@ class mDNSPlugin : Plugin() {
|
|
|
72
80
|
|
|
73
81
|
// ----------------------- API -----------------------
|
|
74
82
|
|
|
83
|
+
@PluginMethod
|
|
84
|
+
fun getPluginPlatform(call: PluginCall) {
|
|
85
|
+
call.resolve(JSObject().put("platform", "android"))
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
/**
|
|
76
89
|
* Start advertising a Bonjour/mDNS service.
|
|
77
90
|
* - type: default "_http._tcp."
|
|
@@ -80,11 +93,11 @@ class mDNSPlugin : Plugin() {
|
|
|
80
93
|
*/
|
|
81
94
|
@PluginMethod
|
|
82
95
|
fun startBroadcast(call: PluginCall) {
|
|
83
|
-
val type = call.getString("type")
|
|
84
|
-
val name = call.getString("name")
|
|
96
|
+
val type = normalizeType(call.getString("type"))
|
|
97
|
+
val name = normalizeName(call.getString("name"))
|
|
85
98
|
val port = call.getInt("port") ?: 0
|
|
86
99
|
|
|
87
|
-
if (port
|
|
100
|
+
if (port !in 1..65535) {
|
|
88
101
|
call.resolve(jsResultBroadcast(false, "", true, "Missing/invalid port"))
|
|
89
102
|
return
|
|
90
103
|
}
|
|
@@ -127,8 +140,8 @@ class mDNSPlugin : Plugin() {
|
|
|
127
140
|
*/
|
|
128
141
|
@PluginMethod
|
|
129
142
|
fun discover(call: PluginCall) {
|
|
130
|
-
val type = call.getString("type")
|
|
131
|
-
val targetName = call.getString("name")
|
|
143
|
+
val type = normalizeType(call.getString("type"))
|
|
144
|
+
val targetName = call.getString("name")?.trim()?.takeIf { it.isNotEmpty() }
|
|
132
145
|
val timeout = call.getInt("timeout") ?: 3000
|
|
133
146
|
|
|
134
147
|
scope.launch {
|
package/dist/docs.json
CHANGED
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"methods": [
|
|
16
|
+
{
|
|
17
|
+
"name": "getPluginPlatform",
|
|
18
|
+
"signature": "() => Promise<MdnsPluginPlatformResult>",
|
|
19
|
+
"parameters": [],
|
|
20
|
+
"returns": "Promise<MdnsPluginPlatformResult>",
|
|
21
|
+
"tags": [
|
|
22
|
+
{
|
|
23
|
+
"name": "returns",
|
|
24
|
+
"text": "Promise resolving to `MdnsPluginPlatformResult`."
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"docs": "Return the platform implementation currently serving plugin calls.",
|
|
28
|
+
"complexTypes": [
|
|
29
|
+
"MdnsPluginPlatformResult"
|
|
30
|
+
],
|
|
31
|
+
"slug": "getpluginplatform"
|
|
32
|
+
},
|
|
16
33
|
{
|
|
17
34
|
"name": "startBroadcast",
|
|
18
35
|
"signature": "(options: MdnsBroadcastOptions) => Promise<MdnsBroadcastResult>",
|
|
@@ -53,7 +70,7 @@
|
|
|
53
70
|
},
|
|
54
71
|
{
|
|
55
72
|
"name": "remarks",
|
|
56
|
-
"text": "The `publishing` flag may be `false` after a successful stop."
|
|
73
|
+
"text": "The `publishing` flag may be `false` after a successful stop.\nStopping is idempotent and is safe to call even if publishing never fully started."
|
|
57
74
|
}
|
|
58
75
|
],
|
|
59
76
|
"docs": "Stop advertising the currently registered service (no-op if none).",
|
|
@@ -98,6 +115,28 @@
|
|
|
98
115
|
"properties": []
|
|
99
116
|
},
|
|
100
117
|
"interfaces": [
|
|
118
|
+
{
|
|
119
|
+
"name": "MdnsPluginPlatformResult",
|
|
120
|
+
"slug": "mdnspluginplatformresult",
|
|
121
|
+
"docs": "Result of getPluginPlatform(). Useful for validating that calls are routed\nthrough the expected platform bridge.",
|
|
122
|
+
"tags": [
|
|
123
|
+
{
|
|
124
|
+
"name": "public"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"methods": [],
|
|
128
|
+
"properties": [
|
|
129
|
+
{
|
|
130
|
+
"name": "platform",
|
|
131
|
+
"tags": [],
|
|
132
|
+
"docs": "Platform implementation that produced this result.",
|
|
133
|
+
"complexTypes": [
|
|
134
|
+
"MdnsPluginPlatform"
|
|
135
|
+
],
|
|
136
|
+
"type": "MdnsPluginPlatform"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
101
140
|
{
|
|
102
141
|
"name": "MdnsBroadcastResult",
|
|
103
142
|
"slug": "mdnsbroadcastresult",
|
|
@@ -145,7 +184,7 @@
|
|
|
145
184
|
"docs": "Options for starting a Bonjour/mDNS advertisement.",
|
|
146
185
|
"tags": [
|
|
147
186
|
{
|
|
148
|
-
"text": "- The `type` **must** end with a dot (`.`). If omitted, the implementation appends it.\n- Instance `name` should be short and human-readable; the OS may append `\" (n)\"` to ensure uniqueness.\n- Android NSD does not expose TXT records to other apps; advertising TXT is supported on iOS.",
|
|
187
|
+
"text": "- The `type` **must** end with a dot (`.`). If omitted, the implementation appends it.\n- Instance `name` should be short and human-readable; the OS may append `\" (n)\"` to ensure uniqueness.\n- Android NSD does not expose TXT records to other apps; advertising TXT is supported on iOS.\n- Native implementations guard publish with a timeout and return `error: true` instead of\n leaving the Promise pending when the OS does not deliver a publish callback.\n- Only one advertisement is active at a time. Starting a new one stops the previous one first.",
|
|
149
188
|
"name": "remarks"
|
|
150
189
|
},
|
|
151
190
|
{
|
|
@@ -359,7 +398,7 @@
|
|
|
359
398
|
"docs": "Options for Bonjour/mDNS discovery.",
|
|
360
399
|
"tags": [
|
|
361
400
|
{
|
|
362
|
-
"text": "- The `
|
|
401
|
+
"text": "- The `name` filter matches the normalized name and also accepts prefix matches\n (to handle OS-added `\" (n)\"` suffixes).\n- When `useNW` is `true` on iOS, discovery uses `NWBrowser` for better P2P behavior.\n- Discovery is timeboxed. Concurrent discovery requests are serialized or safely replace\n the previous native session so old callbacks cannot complete a newer request.",
|
|
363
402
|
"name": "remarks"
|
|
364
403
|
},
|
|
365
404
|
{
|
|
@@ -425,6 +464,29 @@
|
|
|
425
464
|
],
|
|
426
465
|
"enums": [],
|
|
427
466
|
"typeAliases": [
|
|
467
|
+
{
|
|
468
|
+
"name": "MdnsPluginPlatform",
|
|
469
|
+
"slug": "mdnspluginplatform",
|
|
470
|
+
"docs": "Runtime implementation that handled a plugin call.",
|
|
471
|
+
"types": [
|
|
472
|
+
{
|
|
473
|
+
"text": "'ios'",
|
|
474
|
+
"complexTypes": []
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"text": "'android'",
|
|
478
|
+
"complexTypes": []
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"text": "'electron'",
|
|
482
|
+
"complexTypes": []
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"text": "'web'",
|
|
486
|
+
"complexTypes": []
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
},
|
|
428
490
|
{
|
|
429
491
|
"name": "MdnsTxt",
|
|
430
492
|
"slug": "mdnstxt",
|