@devioarts/capacitor-mdns 0.0.2 → 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/Package.swift +1 -1
- package/README.md +54 -67
- package/android/build.gradle +11 -11
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +217 -51
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +20 -9
- package/dist/docs.json +65 -3
- package/dist/esm/definitions.d.ts +28 -2
- 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 +9 -4
- 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 +47 -22
package/Package.swift
CHANGED
|
@@ -10,7 +10,7 @@ let package = Package(
|
|
|
10
10
|
targets: ["mDNSPlugin"])
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
14
14
|
],
|
|
15
15
|
targets: [
|
|
16
16
|
.target(
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -11,7 +11,7 @@ buildscript {
|
|
|
11
11
|
mavenCentral()
|
|
12
12
|
}
|
|
13
13
|
dependencies {
|
|
14
|
-
classpath 'com.android.tools.build:gradle:8.
|
|
14
|
+
classpath 'com.android.tools.build:gradle:8.13.2'
|
|
15
15
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -20,23 +20,23 @@ apply plugin: 'com.android.library'
|
|
|
20
20
|
apply plugin: 'org.jetbrains.kotlin.android'
|
|
21
21
|
|
|
22
22
|
android {
|
|
23
|
-
namespace "com.devioarts.capacitor.mdns"
|
|
24
|
-
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion :
|
|
23
|
+
namespace = "com.devioarts.capacitor.mdns"
|
|
24
|
+
compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
|
|
25
25
|
defaultConfig {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
versionCode 1
|
|
29
|
-
versionName "1.0"
|
|
30
|
-
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
26
|
+
minSdk = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
27
|
+
targetSdk = project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
|
|
28
|
+
versionCode = 1
|
|
29
|
+
versionName = "1.0"
|
|
30
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
31
31
|
}
|
|
32
32
|
buildTypes {
|
|
33
33
|
release {
|
|
34
|
-
minifyEnabled false
|
|
34
|
+
minifyEnabled = false
|
|
35
35
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
abortOnError false
|
|
38
|
+
lint {
|
|
39
|
+
abortOnError = false
|
|
40
40
|
}
|
|
41
41
|
compileOptions {
|
|
42
42
|
sourceCompatibility JavaVersion.VERSION_21
|
|
@@ -4,9 +4,13 @@ package com.devioarts.capacitor.mdns
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.net.nsd.NsdManager
|
|
6
6
|
import android.net.nsd.NsdServiceInfo
|
|
7
|
+
import android.os.Build
|
|
7
8
|
import android.os.Handler
|
|
8
9
|
import android.os.Looper
|
|
9
10
|
import kotlinx.coroutines.*
|
|
11
|
+
import kotlinx.coroutines.sync.Mutex
|
|
12
|
+
import kotlinx.coroutines.sync.withLock
|
|
13
|
+
import java.util.concurrent.Executor
|
|
10
14
|
import java.util.concurrent.atomic.AtomicReference
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -29,27 +33,42 @@ class mDNS(
|
|
|
29
33
|
context: Context,
|
|
30
34
|
private val externalScope: CoroutineScope? = null
|
|
31
35
|
) {
|
|
36
|
+
private companion object {
|
|
37
|
+
const val PUBLISH_TIMEOUT_MS = 5000L
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
/** Normalized service representation returned to the bridge. */
|
|
33
41
|
data class MdnsService(
|
|
34
42
|
val name: String,
|
|
35
43
|
val type: String,
|
|
36
|
-
val
|
|
44
|
+
val hosts: List<String>, // numeric addresses (v4/v6)
|
|
37
45
|
val port: Int
|
|
38
46
|
)
|
|
39
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
|
+
|
|
40
57
|
/** NSD system service. */
|
|
41
58
|
private val nsd: NsdManager =
|
|
42
59
|
context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
|
43
60
|
|
|
44
61
|
/** Keep strong references to listeners so they outlive registration/discovery. */
|
|
45
|
-
private
|
|
62
|
+
private var activeRegistration: RegistrationSession? = null
|
|
46
63
|
private val discListenerRef = AtomicReference<NsdManager.DiscoveryListener?>(null)
|
|
47
64
|
|
|
48
65
|
/** Optional external scope (from plugin); otherwise create our own on Main. */
|
|
49
66
|
private val scope = externalScope ?: CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
67
|
+
private val discoverMutex = Mutex()
|
|
50
68
|
|
|
51
69
|
/** Main looper utilities for non-suspending entry points. */
|
|
52
70
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
71
|
+
private val mainExecutor = Executor { command -> mainHandler.post(command) }
|
|
53
72
|
private inline fun runOnMain(crossinline block: () -> Unit) {
|
|
54
73
|
if (Looper.myLooper() === Looper.getMainLooper()) block() else mainHandler.post { block() }
|
|
55
74
|
}
|
|
@@ -70,29 +89,58 @@ class mDNS(
|
|
|
70
89
|
onSuccess: (String) -> Unit,
|
|
71
90
|
onError: (Throwable) -> Unit
|
|
72
91
|
) {
|
|
73
|
-
require(port
|
|
92
|
+
require(port in 1..65535) { "Port must be between 1 and 65535" }
|
|
74
93
|
val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
|
|
75
94
|
|
|
76
95
|
runOnMain {
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
try {
|
|
97
|
+
// Restarting while publish is pending must complete the old JS promise.
|
|
98
|
+
stopActiveRegistration("Replaced by a new broadcast request")
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
val info = NsdServiceInfo().apply {
|
|
101
|
+
serviceType = type
|
|
102
|
+
serviceName = name
|
|
103
|
+
setPort(port)
|
|
104
|
+
}
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
121
|
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|
|
133
|
+
|
|
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
|
+
}
|
|
96
144
|
}
|
|
97
145
|
}
|
|
98
146
|
|
|
@@ -101,7 +149,7 @@ class mDNS(
|
|
|
101
149
|
*/
|
|
102
150
|
fun stopBroadcast() {
|
|
103
151
|
runOnMain {
|
|
104
|
-
|
|
152
|
+
stopActiveRegistration("Publish stopped before completion")
|
|
105
153
|
}
|
|
106
154
|
}
|
|
107
155
|
|
|
@@ -124,12 +172,13 @@ class mDNS(
|
|
|
124
172
|
typeRaw: String,
|
|
125
173
|
targetName: String? = null,
|
|
126
174
|
timeoutMs: Int = 3000
|
|
127
|
-
): List<MdnsService> = withContext(Dispatchers.Main.immediate) {
|
|
175
|
+
): List<MdnsService> = discoverMutex.withLock { withContext(Dispatchers.Main.immediate) {
|
|
128
176
|
val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
|
|
129
177
|
|
|
130
178
|
// Prepare state
|
|
131
179
|
discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
|
|
132
180
|
val found = mutableListOf<MdnsService>()
|
|
181
|
+
val serviceInfoCallbacks = mutableListOf<NsdManager.ServiceInfoCallback>()
|
|
133
182
|
val result = CompletableDeferred<List<MdnsService>>()
|
|
134
183
|
|
|
135
184
|
// Normalization for "(n)" suffix appended by the OS.
|
|
@@ -139,40 +188,93 @@ class mDNS(
|
|
|
139
188
|
val t = normalize(targetName ?: return true) // if no target, accept all
|
|
140
189
|
return (c == t) || c.startsWith(t)
|
|
141
190
|
}
|
|
191
|
+
fun upsert(item: MdnsService) {
|
|
192
|
+
val index = found.indexOfFirst { it.name == item.name && it.type == item.type && it.port == item.port }
|
|
193
|
+
if (index >= 0) found[index] = item else found.add(item)
|
|
194
|
+
}
|
|
195
|
+
fun unregisterServiceInfoCallbacks() {
|
|
196
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return
|
|
197
|
+
serviceInfoCallbacks.toList().forEach { safeUnregisterServiceInfoCallback(it) }
|
|
198
|
+
serviceInfoCallbacks.clear()
|
|
199
|
+
}
|
|
200
|
+
fun completeWithCurrentResults(discoveryListener: NsdManager.DiscoveryListener) {
|
|
201
|
+
unregisterServiceInfoCallbacks()
|
|
202
|
+
safeStopDiscovery(discoveryListener)
|
|
203
|
+
discListenerRef.compareAndSet(discoveryListener, null)
|
|
204
|
+
if (!result.isCompleted) result.complete(found.toList())
|
|
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
|
+
}
|
|
212
|
+
|
|
213
|
+
@Suppress("DEPRECATION")
|
|
214
|
+
fun resolveLegacy(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
|
|
215
|
+
try {
|
|
216
|
+
nsd.resolveService(si, object : NsdManager.ResolveListener {
|
|
217
|
+
override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
|
|
218
|
+
|
|
219
|
+
override fun onServiceResolved(s: NsdServiceInfo) {
|
|
220
|
+
if (result.isCompleted) return
|
|
221
|
+
upsert(toMdnsService(s))
|
|
222
|
+
|
|
223
|
+
// Early-exit for exact/prefix target match.
|
|
224
|
+
if (targetName != null && matchesTarget(s.serviceName)) {
|
|
225
|
+
completeWithCurrentResults(discoveryListener)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
} catch (_: Throwable) {
|
|
230
|
+
// Some Android releases throw when the NSD daemon is busy; ignore this candidate.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fun registerServiceInfoCallback(si: NsdServiceInfo, discoveryListener: NsdManager.DiscoveryListener) {
|
|
235
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
236
|
+
resolveLegacy(si, discoveryListener)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
val callback = object : NsdManager.ServiceInfoCallback {
|
|
241
|
+
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { /* ignore */ }
|
|
242
|
+
override fun onServiceInfoCallbackUnregistered() { /* no-op */ }
|
|
243
|
+
override fun onServiceLost() { /* no-op */ }
|
|
244
|
+
|
|
245
|
+
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
|
246
|
+
if (result.isCompleted) return
|
|
247
|
+
upsert(toMdnsService(serviceInfo))
|
|
248
|
+
|
|
249
|
+
// Early-exit for exact/prefix target match.
|
|
250
|
+
if (targetName != null && matchesTarget(serviceInfo.serviceName)) {
|
|
251
|
+
completeWithCurrentResults(discoveryListener)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
serviceInfoCallbacks.add(callback)
|
|
257
|
+
try {
|
|
258
|
+
nsd.registerServiceInfoCallback(si, mainExecutor, callback)
|
|
259
|
+
} catch (_: Throwable) {
|
|
260
|
+
serviceInfoCallbacks.remove(callback)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
142
263
|
|
|
143
264
|
val listener = object : NsdManager.DiscoveryListener {
|
|
144
265
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
|
145
|
-
|
|
146
|
-
safeStopDiscovery(this)
|
|
147
|
-
if (!result.isCompleted) result.complete(found.toList())
|
|
266
|
+
completeWithError(this, IllegalStateException("Discovery failed to start: $errorCode"))
|
|
148
267
|
}
|
|
149
268
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { /* no-op */ }
|
|
150
269
|
override fun onDiscoveryStarted(serviceType: String) { /* no-op */ }
|
|
151
270
|
override fun onDiscoveryStopped(serviceType: String) { /* no-op */ }
|
|
152
271
|
|
|
153
272
|
override fun onServiceFound(si: NsdServiceInfo) {
|
|
273
|
+
if (result.isCompleted) return
|
|
154
274
|
if (targetName != null && !matchesTarget(si.serviceName)) return
|
|
155
275
|
|
|
156
276
|
val discoveryListener = this
|
|
157
|
-
|
|
158
|
-
override fun onResolveFailed(s: NsdServiceInfo, errorCode: Int) { /* ignore */ }
|
|
159
|
-
|
|
160
|
-
override fun onServiceResolved(s: NsdServiceInfo) {
|
|
161
|
-
val item = MdnsService(
|
|
162
|
-
name = s.serviceName,
|
|
163
|
-
type = s.serviceType,
|
|
164
|
-
host = s.host?.hostAddress,
|
|
165
|
-
port = s.port
|
|
166
|
-
)
|
|
167
|
-
found.add(item)
|
|
168
|
-
|
|
169
|
-
// Early-exit for exact/prefix target match.
|
|
170
|
-
if (targetName != null && matchesTarget(s.serviceName)) {
|
|
171
|
-
safeStopDiscovery(discoveryListener)
|
|
172
|
-
if (!result.isCompleted) result.complete(found.toList())
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
})
|
|
277
|
+
registerServiceInfoCallback(si, discoveryListener)
|
|
176
278
|
}
|
|
177
279
|
|
|
178
280
|
override fun onServiceLost(serviceInfo: NsdServiceInfo) { /* no-op */ }
|
|
@@ -180,12 +282,16 @@ class mDNS(
|
|
|
180
282
|
|
|
181
283
|
// Start discovery and schedule timeout
|
|
182
284
|
discListenerRef.set(listener)
|
|
183
|
-
|
|
285
|
+
try {
|
|
286
|
+
nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
287
|
+
} catch (t: Throwable) {
|
|
288
|
+
discListenerRef.compareAndSet(listener, null)
|
|
289
|
+
throw t
|
|
290
|
+
}
|
|
184
291
|
|
|
185
292
|
val timeoutJob = scope.launch {
|
|
186
|
-
delay(timeoutMs.toLong())
|
|
187
|
-
|
|
188
|
-
if (!result.isCompleted) result.complete(found.toList())
|
|
293
|
+
delay(timeoutMs.coerceAtLeast(0).toLong())
|
|
294
|
+
completeWithCurrentResults(listener)
|
|
189
295
|
}
|
|
190
296
|
|
|
191
297
|
try {
|
|
@@ -193,17 +299,20 @@ class mDNS(
|
|
|
193
299
|
} finally {
|
|
194
300
|
// Cleanup in all cases (early-exit or timeout)
|
|
195
301
|
timeoutJob.cancel()
|
|
196
|
-
|
|
302
|
+
unregisterServiceInfoCallbacks()
|
|
303
|
+
if (discListenerRef.compareAndSet(listener, null)) safeStopDiscovery(listener)
|
|
197
304
|
}
|
|
198
|
-
}
|
|
305
|
+
} }
|
|
199
306
|
|
|
200
307
|
/**
|
|
201
308
|
* Close the manager and release any outstanding NSD listeners.
|
|
202
309
|
* Safe to call multiple times; also invoked by the plugin's handleOnDestroy().
|
|
203
310
|
*/
|
|
204
311
|
fun close() {
|
|
205
|
-
|
|
206
|
-
|
|
312
|
+
runOnMain {
|
|
313
|
+
stopActiveRegistration("mDNS manager closed before publish completed")
|
|
314
|
+
discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
|
|
315
|
+
}
|
|
207
316
|
if (externalScope == null) scope.cancel()
|
|
208
317
|
}
|
|
209
318
|
|
|
@@ -214,8 +323,65 @@ class mDNS(
|
|
|
214
323
|
try { nsd.unregisterService(l) } catch (_: Throwable) {}
|
|
215
324
|
}
|
|
216
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
|
+
|
|
217
358
|
/** Best-effort stop discovery; NSD may throw if discovery is not active. */
|
|
218
359
|
private fun safeStopDiscovery(l: NsdManager.DiscoveryListener) {
|
|
219
360
|
try { nsd.stopServiceDiscovery(l) } catch (_: Throwable) {}
|
|
220
361
|
}
|
|
362
|
+
|
|
363
|
+
/** Best-effort stop service info updates; NSD may throw if not registered. */
|
|
364
|
+
private fun safeUnregisterServiceInfoCallback(l: NsdManager.ServiceInfoCallback) {
|
|
365
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
366
|
+
try { nsd.unregisterServiceInfoCallback(l) } catch (_: Throwable) {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private fun toMdnsService(s: NsdServiceInfo): MdnsService =
|
|
371
|
+
MdnsService(
|
|
372
|
+
name = s.serviceName,
|
|
373
|
+
type = s.serviceType,
|
|
374
|
+
hosts = hostAddresses(s),
|
|
375
|
+
port = s.port
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
private fun hostAddresses(s: NsdServiceInfo): List<String> =
|
|
379
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
380
|
+
s.hostAddresses.mapNotNull { it.hostAddress }
|
|
381
|
+
} else {
|
|
382
|
+
legacyHostAddress(s)?.let { listOf(it) } ?: emptyList()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@Suppress("DEPRECATION")
|
|
386
|
+
private fun legacyHostAddress(s: NsdServiceInfo): String? = s.host?.hostAddress
|
|
221
387
|
}
|