@devioarts/capacitor-mdns 0.0.2
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/DevioartsCapacitorMdns.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +248 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +221 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +157 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +457 -0
- package/dist/esm/definitions.d.ts +190 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +17 -0
- package/dist/esm/web.js +41 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +55 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +58 -0
- package/dist/plugin.js.map +1 -0
- package/electron/global.d.ts +28 -0
- package/electron/mdns-bridge.cjs +29 -0
- package/electron/mdns.ts +267 -0
- package/ios/Sources/mDNSPlugin/mDNS.swift +436 -0
- package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +125 -0
- package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +15 -0
- package/package.json +96 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'DevioartsCapacitorMdns'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = package['repository']['url']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.ios.deployment_target = '14.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.swift_version = '5.1'
|
|
17
|
+
end
|
package/Package.swift
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "DevioartsCapacitorMdns",
|
|
6
|
+
platforms: [.iOS(.v14)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "DevioartsCapacitorMdns",
|
|
10
|
+
targets: ["mDNSPlugin"])
|
|
11
|
+
],
|
|
12
|
+
dependencies: [
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0")
|
|
14
|
+
],
|
|
15
|
+
targets: [
|
|
16
|
+
.target(
|
|
17
|
+
name: "mDNSPlugin",
|
|
18
|
+
dependencies: [
|
|
19
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
],
|
|
22
|
+
path: "ios/Sources/mDNSPlugin"),
|
|
23
|
+
.testTarget(
|
|
24
|
+
name: "mDNSPluginTests",
|
|
25
|
+
dependencies: ["mDNSPlugin"],
|
|
26
|
+
path: "ios/Tests/mDNSPluginTests")
|
|
27
|
+
]
|
|
28
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# @devioarts/capacitor-mdns
|
|
2
|
+
|
|
3
|
+
mDNS plugin for Capacitor that supports Bonjour/mDNS advertisements and discovery.
|
|
4
|
+
|
|
5
|
+
#### Supported platforms: ✓ iOS ✓ Android ✓ Electron
|
|
6
|
+
#### Demo (sources): [application](https://github.com/devioarts/capacitor-examples/tree/main/capacitor-mdns) or directly [file](https://github.com/devioarts/capacitor-examples/blob/main/capacitor-mdns/src/Playground.tsx)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @devioarts/capacitor-mdns
|
|
12
|
+
npx cap sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Android
|
|
16
|
+
#### /android/app/src/main/AndroidManifest.xml
|
|
17
|
+
```xml
|
|
18
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## iOS
|
|
22
|
+
#### /ios/App/App/Info.plist
|
|
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
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
## ElectronJS
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
npm i bonjour-service@1.3.0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> Implementation example was developed on [capacitor-electron](https://github.com/devioarts/capacitor-examples/tree/main/capacitor-electron)
|
|
45
|
+
> base, if you run electron differently, you may need to adjust the code.
|
|
46
|
+
|
|
47
|
+
#### /electron/main.ts
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// ...
|
|
51
|
+
import { mDNS } from '@devioarts/capacitor-mdns/electron/mdns'
|
|
52
|
+
// ...
|
|
53
|
+
const mdns = new mDNS();
|
|
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
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
<docgen-index>
|
|
90
|
+
|
|
91
|
+
* [`startBroadcast(...)`](#startbroadcast)
|
|
92
|
+
* [`stopBroadcast()`](#stopbroadcast)
|
|
93
|
+
* [`discover(...)`](#discover)
|
|
94
|
+
* [Interfaces](#interfaces)
|
|
95
|
+
* [Type Aliases](#type-aliases)
|
|
96
|
+
|
|
97
|
+
</docgen-index>
|
|
98
|
+
|
|
99
|
+
<docgen-api>
|
|
100
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
101
|
+
|
|
102
|
+
Public API surface of the Capacitor mDNS plugin.
|
|
103
|
+
|
|
104
|
+
### startBroadcast(...)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
startBroadcast(options: MdnsBroadcastOptions) => Promise<MdnsBroadcastResult>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Start advertising a Bonjour/mDNS service.
|
|
111
|
+
|
|
112
|
+
| Param | Type | Description |
|
|
113
|
+
| ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
114
|
+
| **`options`** | <code><a href="#mdnsbroadcastoptions">MdnsBroadcastOptions</a></code> | - {@link <a href="#mdnsbroadcastoptions">MdnsBroadcastOptions</a>} |
|
|
115
|
+
|
|
116
|
+
**Returns:** <code>Promise<<a href="#mdnsbroadcastresult">MdnsBroadcastResult</a>></code>
|
|
117
|
+
|
|
118
|
+
--------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### stopBroadcast()
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
stopBroadcast() => Promise<MdnsStopResult>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Stop advertising the currently registered service (no-op if none).
|
|
128
|
+
|
|
129
|
+
**Returns:** <code>Promise<<a href="#mdnsstopresult">MdnsStopResult</a>></code>
|
|
130
|
+
|
|
131
|
+
--------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
### discover(...)
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
discover(options?: MdnsDiscoverOptions | undefined) => Promise<MdnsDiscoverResult>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Discover services of a given type and optionally filter by instance name.
|
|
141
|
+
|
|
142
|
+
| Param | Type | Description |
|
|
143
|
+
| ------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
144
|
+
| **`options`** | <code><a href="#mdnsdiscoveroptions">MdnsDiscoverOptions</a></code> | - {@link <a href="#mdnsdiscoveroptions">MdnsDiscoverOptions</a>} |
|
|
145
|
+
|
|
146
|
+
**Returns:** <code>Promise<<a href="#mdnsdiscoverresult">MdnsDiscoverResult</a>></code>
|
|
147
|
+
|
|
148
|
+
--------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
### Interfaces
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
#### MdnsBroadcastResult
|
|
155
|
+
|
|
156
|
+
Result of startBroadcast(). Indicates whether advertising is active
|
|
157
|
+
and the final service name. On failure, `error` is true and `errorMessage`
|
|
158
|
+
describes the issue.
|
|
159
|
+
|
|
160
|
+
| Prop | Type | Description |
|
|
161
|
+
| ------------------ | --------------------------- | -------------------------------------------------------------------- |
|
|
162
|
+
| **`error`** | <code>boolean</code> | True if the operation failed. |
|
|
163
|
+
| **`errorMessage`** | <code>string \| null</code> | Error description or null on success. |
|
|
164
|
+
| **`name`** | <code>string</code> | Final (possibly uniquified) service instance name. Empty on failure. |
|
|
165
|
+
| **`publishing`** | <code>boolean</code> | Whether the advertiser is currently active. |
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#### MdnsBroadcastOptions
|
|
169
|
+
|
|
170
|
+
Options for starting a Bonjour/mDNS advertisement.
|
|
171
|
+
|
|
172
|
+
| Prop | Type | Description |
|
|
173
|
+
| ---------- | ------------------------------------------- | --------------------------------------------- |
|
|
174
|
+
| **`type`** | <code>string</code> | Service type (including the trailing dot). |
|
|
175
|
+
| **`name`** | <code>string</code> | Service instance name. |
|
|
176
|
+
| **`port`** | <code>number</code> | TCP port to advertise. |
|
|
177
|
+
| **`txt`** | <code><a href="#mdnstxt">MdnsTxt</a></code> | Optional TXT key–value pairs (UTF-8 strings). |
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
#### MdnsStopResult
|
|
181
|
+
|
|
182
|
+
Result of stopBroadcast(). Indicates whether the advertiser is active
|
|
183
|
+
after the call (normally false) and includes error information.
|
|
184
|
+
|
|
185
|
+
| Prop | Type | Description |
|
|
186
|
+
| ------------------ | --------------------------- | ------------------------------------------------------------------- |
|
|
187
|
+
| **`error`** | <code>boolean</code> | True if an error occurred while stopping. |
|
|
188
|
+
| **`errorMessage`** | <code>string \| null</code> | Error description or null on success. |
|
|
189
|
+
| **`publishing`** | <code>boolean</code> | Whether the advertiser remains active (should be false on success). |
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#### MdnsDiscoverResult
|
|
193
|
+
|
|
194
|
+
Result of discover(). Contains normalized services and error information.
|
|
195
|
+
|
|
196
|
+
| Prop | Type | Description |
|
|
197
|
+
| ------------------- | --------------------------- | ------------------------------------------------------------------------------ |
|
|
198
|
+
| **`error`** | <code>boolean</code> | True if discovery encountered an error (partial results may still be present). |
|
|
199
|
+
| **`errorMessage`** | <code>string \| null</code> | Error description or null when no error occurred. |
|
|
200
|
+
| **`servicesFound`** | <code>number</code> | Convenience count equal to services.length. |
|
|
201
|
+
| **`services`** | <code>MdnsService[]</code> | Normalized list of discovered services. |
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
#### MdnsService
|
|
205
|
+
|
|
206
|
+
Normalized description of a discovered Bonjour/mDNS service.
|
|
207
|
+
Returned from {@link mDNSPlugin.discover}.
|
|
208
|
+
|
|
209
|
+
| Prop | Type | Description |
|
|
210
|
+
| ------------ | ------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
211
|
+
| **`name`** | <code>string</code> | Instance name of the service (may be uniqued by the OS, e.g. "My App (2)"). |
|
|
212
|
+
| **`type`** | <code>string</code> | Full service type including the trailing dot, e.g. `"_http._tcp."`. |
|
|
213
|
+
| **`domain`** | <code>string</code> | Service domain; typically `"local."`. |
|
|
214
|
+
| **`port`** | <code>number</code> | TCP port the service advertises. |
|
|
215
|
+
| **`hosts`** | <code>string[]</code> | Resolved numeric IP addresses (IPv4/IPv6). |
|
|
216
|
+
| **`txt`** | <code><a href="#mdnstxt">MdnsTxt</a></code> | TXT dictionary (key → value). Usually present on iOS; Android NSD does not populate this. |
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
#### MdnsDiscoverOptions
|
|
220
|
+
|
|
221
|
+
Options for Bonjour/mDNS discovery.
|
|
222
|
+
|
|
223
|
+
| Prop | Type | Description |
|
|
224
|
+
| ------------- | -------------------- | ---------------------------------------------------------------- |
|
|
225
|
+
| **`type`** | <code>string</code> | Service type (including the trailing dot). |
|
|
226
|
+
| **`name`** | <code>string</code> | Optional instance name filter (prefix-safe). |
|
|
227
|
+
| **`timeout`** | <code>number</code> | Discovery timeout in milliseconds. |
|
|
228
|
+
| **`useNW`** | <code>boolean</code> | iOS-only hint to use `NWBrowser` instead of `NetServiceBrowser`. |
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
### Type Aliases
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
#### MdnsTxt
|
|
235
|
+
|
|
236
|
+
Key–value map for TXT records of a Bonjour/mDNS service.
|
|
237
|
+
Values are UTF-8 strings; binary payloads are not supported by this API.
|
|
238
|
+
|
|
239
|
+
<code><a href="#record">Record</a><string, string></code>
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
#### Record
|
|
243
|
+
|
|
244
|
+
Construct a type with a set of properties K of type T
|
|
245
|
+
|
|
246
|
+
<code>{
|
|
247
|
[P in K]: T;
|
|
1
248
|
}</code>
|
|
249
|
+
|
|
250
|
+
</docgen-api>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath 'com.android.tools.build:gradle:8.7.2'
|
|
15
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: 'com.android.library'
|
|
20
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
21
|
+
|
|
22
|
+
android {
|
|
23
|
+
namespace "com.devioarts.capacitor.mdns"
|
|
24
|
+
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
|
|
25
|
+
defaultConfig {
|
|
26
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
27
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
|
|
28
|
+
versionCode 1
|
|
29
|
+
versionName "1.0"
|
|
30
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
31
|
+
}
|
|
32
|
+
buildTypes {
|
|
33
|
+
release {
|
|
34
|
+
minifyEnabled false
|
|
35
|
+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
lintOptions {
|
|
39
|
+
abortOnError false
|
|
40
|
+
}
|
|
41
|
+
compileOptions {
|
|
42
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
43
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
kotlinOptions {
|
|
47
|
+
jvmTarget = '21'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
repositories {
|
|
52
|
+
google()
|
|
53
|
+
mavenCentral()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
dependencies {
|
|
58
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
59
|
+
implementation project(':capacitor-android')
|
|
60
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
61
|
+
testImplementation "junit:junit:$junitVersion"
|
|
62
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
63
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
64
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
package com.devioarts.capacitor.mdns
|
|
2
|
+
// English-only code and comments.
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.net.nsd.NsdManager
|
|
6
|
+
import android.net.nsd.NsdServiceInfo
|
|
7
|
+
import android.os.Handler
|
|
8
|
+
import android.os.Looper
|
|
9
|
+
import kotlinx.coroutines.*
|
|
10
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Thin Android NSD (Bonjour/mDNS) wrapper used by the Capacitor bridge.
|
|
14
|
+
*
|
|
15
|
+
* Responsibilities:
|
|
16
|
+
* - Register/unregister (advertise/stop) a single service.
|
|
17
|
+
* - Discover services of a given type and resolve them to host/port.
|
|
18
|
+
* - Optional exact-or-prefix name filter (iOS-like behavior, handles " (n)" suffix).
|
|
19
|
+
* - Timebox discovery; early-exit when the target match is resolved.
|
|
20
|
+
*
|
|
21
|
+
* Threading:
|
|
22
|
+
* - All public APIs are main-thread safe. Calls hop to the main looper internally,
|
|
23
|
+
* since NSD APIs deliver callbacks on the main thread and expect a looper.
|
|
24
|
+
*
|
|
25
|
+
* Platform notes:
|
|
26
|
+
* - Android NSD has no public API for TXT records.
|
|
27
|
+
*/
|
|
28
|
+
class mDNS(
|
|
29
|
+
context: Context,
|
|
30
|
+
private val externalScope: CoroutineScope? = null
|
|
31
|
+
) {
|
|
32
|
+
/** Normalized service representation returned to the bridge. */
|
|
33
|
+
data class MdnsService(
|
|
34
|
+
val name: String,
|
|
35
|
+
val type: String,
|
|
36
|
+
val host: String?, // numeric address (v4/v6) or null if resolve fails
|
|
37
|
+
val port: Int
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
/** NSD system service. */
|
|
41
|
+
private val nsd: NsdManager =
|
|
42
|
+
context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
|
43
|
+
|
|
44
|
+
/** Keep strong references to listeners so they outlive registration/discovery. */
|
|
45
|
+
private val regListenerRef = AtomicReference<NsdManager.RegistrationListener?>(null)
|
|
46
|
+
private val discListenerRef = AtomicReference<NsdManager.DiscoveryListener?>(null)
|
|
47
|
+
|
|
48
|
+
/** Optional external scope (from plugin); otherwise create our own on Main. */
|
|
49
|
+
private val scope = externalScope ?: CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
50
|
+
|
|
51
|
+
/** Main looper utilities for non-suspending entry points. */
|
|
52
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
53
|
+
private inline fun runOnMain(crossinline block: () -> Unit) {
|
|
54
|
+
if (Looper.myLooper() === Looper.getMainLooper()) block() else mainHandler.post { block() }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start advertising a service. If already registered, the previous one is unregistered first.
|
|
59
|
+
*
|
|
60
|
+
* @param typeRaw Service type; trailing dot appended if missing (e.g., "_http._tcp.")
|
|
61
|
+
* @param name Instance name; OS may append " (n)" to ensure uniqueness.
|
|
62
|
+
* @param port TCP port (> 0).
|
|
63
|
+
* @param onSuccess Called with the (potentially uniquified) registered name.
|
|
64
|
+
* @param onError Called on registration failure.
|
|
65
|
+
*/
|
|
66
|
+
fun broadcast(
|
|
67
|
+
typeRaw: String,
|
|
68
|
+
name: String,
|
|
69
|
+
port: Int,
|
|
70
|
+
onSuccess: (String) -> Unit,
|
|
71
|
+
onError: (Throwable) -> Unit
|
|
72
|
+
) {
|
|
73
|
+
require(port > 0) { "Port must be > 0" }
|
|
74
|
+
val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
|
|
75
|
+
|
|
76
|
+
runOnMain {
|
|
77
|
+
// Ensure a clean state before registering again.
|
|
78
|
+
regListenerRef.getAndSet(null)?.let { safeUnregister(it) }
|
|
79
|
+
|
|
80
|
+
val info = NsdServiceInfo().apply {
|
|
81
|
+
serviceType = type
|
|
82
|
+
serviceName = name
|
|
83
|
+
setPort(port)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
val listener = object : NsdManager.RegistrationListener {
|
|
87
|
+
override fun onServiceRegistered(nsi: NsdServiceInfo) = onSuccess(nsi.serviceName)
|
|
88
|
+
override fun onRegistrationFailed(nsi: NsdServiceInfo, errorCode: Int) =
|
|
89
|
+
onError(IllegalStateException("Registration failed: $errorCode"))
|
|
90
|
+
override fun onServiceUnregistered(nsi: NsdServiceInfo) { /* no-op */ }
|
|
91
|
+
override fun onUnregistrationFailed(nsi: NsdServiceInfo, errorCode: Int) { /* no-op */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
regListenerRef.set(listener)
|
|
95
|
+
nsd.registerService(info, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Stop advertising the currently registered service (if any). Safe to call multiple times.
|
|
101
|
+
*/
|
|
102
|
+
fun stopBroadcast() {
|
|
103
|
+
runOnMain {
|
|
104
|
+
regListenerRef.getAndSet(null)?.let { safeUnregister(it) }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Discover services, optionally filtering by instance name using a normalized exact-or-prefix match.
|
|
110
|
+
*
|
|
111
|
+
* Behavior:
|
|
112
|
+
* - Runs on the main dispatcher.
|
|
113
|
+
* - Resolves candidates to host/port.
|
|
114
|
+
* - If `targetName` is provided, compare using normalized names and accept exact OR prefix match.
|
|
115
|
+
* - Early-exits when a matching service is resolved (stops discovery immediately).
|
|
116
|
+
* - Otherwise returns what was found when `timeoutMs` elapses.
|
|
117
|
+
*
|
|
118
|
+
* @param typeRaw Service type; trailing dot appended if missing.
|
|
119
|
+
* @param targetName Optional instance name for normalized exact/prefix match.
|
|
120
|
+
* @param timeoutMs Timebox for discovery+resolve.
|
|
121
|
+
* @return List of resolved services accumulated (or early-exit match).
|
|
122
|
+
*/
|
|
123
|
+
suspend fun discover(
|
|
124
|
+
typeRaw: String,
|
|
125
|
+
targetName: String? = null,
|
|
126
|
+
timeoutMs: Int = 3000
|
|
127
|
+
): List<MdnsService> = withContext(Dispatchers.Main.immediate) {
|
|
128
|
+
val type = if (typeRaw.endsWith(".")) typeRaw else "$typeRaw."
|
|
129
|
+
|
|
130
|
+
// Prepare state
|
|
131
|
+
discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
|
|
132
|
+
val found = mutableListOf<MdnsService>()
|
|
133
|
+
val result = CompletableDeferred<List<MdnsService>>()
|
|
134
|
+
|
|
135
|
+
// Normalization for "(n)" suffix appended by the OS.
|
|
136
|
+
fun normalize(s: String): String = s.replace(Regex(" \\(\\d+\\)\$"), "")
|
|
137
|
+
fun matchesTarget(candidate: String): Boolean {
|
|
138
|
+
val c = normalize(candidate)
|
|
139
|
+
val t = normalize(targetName ?: return true) // if no target, accept all
|
|
140
|
+
return (c == t) || c.startsWith(t)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
val listener = object : NsdManager.DiscoveryListener {
|
|
144
|
+
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
|
145
|
+
// Fail fast: stop discovery and complete with whatever we have (likely empty).
|
|
146
|
+
safeStopDiscovery(this)
|
|
147
|
+
if (!result.isCompleted) result.complete(found.toList())
|
|
148
|
+
}
|
|
149
|
+
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { /* no-op */ }
|
|
150
|
+
override fun onDiscoveryStarted(serviceType: String) { /* no-op */ }
|
|
151
|
+
override fun onDiscoveryStopped(serviceType: String) { /* no-op */ }
|
|
152
|
+
|
|
153
|
+
override fun onServiceFound(si: NsdServiceInfo) {
|
|
154
|
+
if (targetName != null && !matchesTarget(si.serviceName)) return
|
|
155
|
+
|
|
156
|
+
val discoveryListener = this
|
|
157
|
+
nsd.resolveService(si, object : NsdManager.ResolveListener {
|
|
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
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override fun onServiceLost(serviceInfo: NsdServiceInfo) { /* no-op */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Start discovery and schedule timeout
|
|
182
|
+
discListenerRef.set(listener)
|
|
183
|
+
nsd.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
184
|
+
|
|
185
|
+
val timeoutJob = scope.launch {
|
|
186
|
+
delay(timeoutMs.toLong())
|
|
187
|
+
safeStopDiscovery(listener)
|
|
188
|
+
if (!result.isCompleted) result.complete(found.toList())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
result.await()
|
|
193
|
+
} finally {
|
|
194
|
+
// Cleanup in all cases (early-exit or timeout)
|
|
195
|
+
timeoutJob.cancel()
|
|
196
|
+
discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Close the manager and release any outstanding NSD listeners.
|
|
202
|
+
* Safe to call multiple times; also invoked by the plugin's handleOnDestroy().
|
|
203
|
+
*/
|
|
204
|
+
fun close() {
|
|
205
|
+
stopBroadcast()
|
|
206
|
+
discListenerRef.getAndSet(null)?.let { safeStopDiscovery(it) }
|
|
207
|
+
if (externalScope == null) scope.cancel()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- Internal helpers ----------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/** Best-effort unregister; NSD may throw if already unregistered. */
|
|
213
|
+
private fun safeUnregister(l: NsdManager.RegistrationListener) {
|
|
214
|
+
try { nsd.unregisterService(l) } catch (_: Throwable) {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Best-effort stop discovery; NSD may throw if discovery is not active. */
|
|
218
|
+
private fun safeStopDiscovery(l: NsdManager.DiscoveryListener) {
|
|
219
|
+
try { nsd.stopServiceDiscovery(l) } catch (_: Throwable) {}
|
|
220
|
+
}
|
|
221
|
+
}
|