I'm using NsdManager in an Android App to discover NSD services published by another device also developed by me. I only do service discovery on Android App (no service registration needed on this side). There are several instances of the same type of service published at the same time on the network.
I started using the sample code provided by Google (https://developer.android.com/training/connect-devices-wirelessly/nsd) but I had fatal errors due to reusing the same resolver object at the same time for more than one service resolution. Then I found several people suggesting to create a new resolver object each time (like in Listener already in use (Service Discovery)).
I did this and the fatal error was replaced by a Resolve Failure error code 3 that meant that the resolve process was active. Better than before, but only the first service was resolved and the rest was ignored due to this failure.
Then I found a person suggesting to give a special treatment to Error Code 3 by resending the resolve request recursively until it eventually becomes resolved ( NSNetworkManager.ResolveListener messages Android).
I implemented this solution in Kotlin and it kind of works but I'm not really satisfied because:
The people of RxBonjour2 have come with a more complex and robust solution but it's too complex for me to follow it: https://github.com/mannodermaus/RxBonjour/blob/2.x/rxbonjour-drivers/rxbonjour-driver-nsdmanager/src/main/kotlin/de/mannodermaus/rxbonjour/drivers/nsdmanager/NsdManagerDiscoveryEngine.kt
I feel frustrated that Google's official examples do not handle these problems correctly. The nsd_chat sample uses a single resolver object and fails when more than one service with the same type is published at the same type on the network.
Can you suggest a better solution? Or any improvements to my code below?
import android.app.Application
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import androidx.lifecycle.AndroidViewModel
import timber.log.Timber
class ViewModel(application: Application) : AndroidViewModel(application) {
// Get application context
private val myAppContext: Context = getApplication<Application>().applicationContext
// Declare DNS-SD related variables for service discovery
var nsdManager: NsdManager? = null
private var discoveryListener: NsdManager.DiscoveryListener? = null
// Constructor for the View Model that is run when the view model is created
init {
// Initialize DNS-SD service discovery
nsdManager = myAppContext.getSystemService(Context.NSD_SERVICE) as NsdManager?
initializeDiscoveryListener()
// Start looking for available services in the network
nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
}
// Instantiate DNS-SD discovery listener
// used to discover available Sonata audio servers on the same network
private fun initializeDiscoveryListener() {
// Instantiate a new DiscoveryListener
discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {
// Called as soon as service discovery begins.
Timber.d("Service discovery started: $regType")
}
override fun onServiceFound(service: NsdServiceInfo) {
// A service was found! Do something with it
Timber.d("Service discovery success: $service")
when {
service.serviceType != NSD_SERVICE_TYPE ->
// Service type is not the one we are looking for
Timber.d("Unknown Service Type: ${service.serviceType}")
service.serviceName.contains(NSD_SERVICE_NAME) ->
// Both service type and service name are the ones we want
// Resolve the service to get all the details
startResolveService(service)
else ->
// Service type is ours but not the service name
// Log message but do nothing else
Timber.d("Unknown Service Name: ${service.serviceName}")
}
}
override fun onServiceLost(service: NsdServiceInfo) {
onNsdServiceLost(service)
}
override fun onDiscoveryStopped(serviceType: String) {
Timber.i("Discovery stopped: $serviceType")
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Timber.e("Start Discovery failed: Error code: $errorCode")
nsdManager?.stopServiceDiscovery(this)
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Timber.e("Stop Discovery failed: Error code: $errorCode")
nsdManager?.stopServiceDiscovery(this)
}
}
}
fun startResolveService(service: NsdServiceInfo) {
val newResolveListener = object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Called when the resolve fails. Use the error code to determine action.
when (errorCode) {
NsdManager.FAILURE_ALREADY_ACTIVE -> {
// Resolver was busy
Timber.d("Resolve failed: $serviceInfo - Already active")
// Just try again...
startResolveService(serviceInfo)
}
else ->
Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")
}
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
onNsdServiceResolved(serviceInfo)
}
}
nsdManager?.resolveService(service, newResolveListener)
}
companion object {
// We'll only search for NDS services of this type
const val NSD_SERVICE_TYPE: String = "_servicetype._tcp."
// and whose names start like this
const val NSD_SERVICE_NAME: String = "ServiceName-"
}
override fun onCleared() {
try {
nsdManager?.stopServiceDiscovery(discoveryListener)
} catch (ignored: Exception) {
// "Service discovery not active on discoveryListener",
// thrown if starting the service discovery was unsuccessful earlier
}
Timber.d("onCleared called")
super.onCleared()
}
fun onNsdServiceResolved(serviceInfo: NsdServiceInfo) {
// Logic to handle a new service
Timber.d("Resolve Succeeded: $serviceInfo")
}
fun onNsdServiceLost(service: NsdServiceInfo) {
// Logic to handle when the network service is no longer available
Timber.d("Service lost: $service")
}
}
I solved the problem by:
To make the solution more generic, I built an NdsHelper abstract class. It has 2 functions that must be overridden: onNsdServiceResolved(NsdServiceInfo) and onNsdServiceLost(NsdServiceInfo).
I'm using Timber for logging messages but you can replace them by the standard Log function.
This is the NsdHelper class (Kotlin code):
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import timber.log.Timber
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
abstract class NsdHelper(val context: Context) {
// Declare DNS-SD related variables for service discovery
val nsdManager: NsdManager? = context.getSystemService(Context.NSD_SERVICE) as NsdManager?
private var discoveryListener: NsdManager.DiscoveryListener? = null
private var resolveListener: NsdManager.ResolveListener? = null
private var resolveListenerBusy = AtomicBoolean(false)
private var pendingNsdServices = ConcurrentLinkedQueue<NsdServiceInfo>()
var resolvedNsdServices: MutableList<NsdServiceInfo> = Collections.synchronizedList(ArrayList<NsdServiceInfo>())
companion object {
// Type of services to look for
const val NSD_SERVICE_TYPE: String = "_myservicetype._tcp."
// Services' Names must start with this
const val NSD_SERVICE_NAME: String = "MyServiceName-"
}
// Initialize Listeners
fun initializeNsd() {
// Initialize only resolve listener
initializeResolveListener()
}
// Instantiate DNS-SD discovery listener
// used to discover available Sonata audio servers on the same network
private fun initializeDiscoveryListener() {
// Instantiate a new DiscoveryListener
discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {
// Called as soon as service discovery begins.
Timber.d("Service discovery started: $regType")
}
override fun onServiceFound(service: NsdServiceInfo) {
// A service was found! Do something with it
Timber.d("Service discovery success: $service")
if ( service.serviceType == NSD_SERVICE_TYPE &&
service.serviceName.startsWith(NSD_SERVICE_NAME) ) {
// Both service type and service name are the ones we want
// If the resolver is free, resolve the service to get all the details
if (resolveListenerBusy.compareAndSet(false, true)) {
nsdManager?.resolveService(service, resolveListener)
}
else {
// Resolver was busy. Add the service to the list of pending services
pendingNsdServices.add(service)
}
}
else {
// Not our service. Log message but do nothing else
Timber.d("Not our Service - Name: ${service.serviceName}, Type: ${service.serviceType}")
}
}
override fun onServiceLost(service: NsdServiceInfo) {
Timber.d("Service lost: $service")
// If the lost service was in the queue of pending services, remove it
var iterator = pendingNsdServices.iterator()
while (iterator.hasNext()) {
if (iterator.next().serviceName == service.serviceName)
iterator.remove()
}
// If the lost service was in the list of resolved services, remove it
synchronized(resolvedNsdServices) {
iterator = resolvedNsdServices.iterator()
while (iterator.hasNext()) {
if (iterator.next().serviceName == service.serviceName)
iterator.remove()
}
}
// Do the rest of the processing for the lost service
onNsdServiceLost(service)
}
override fun onDiscoveryStopped(serviceType: String) {
Timber.i("Discovery stopped: $serviceType")
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Timber.e("Start Discovery failed: Error code: $errorCode")
stopDiscovery()
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Timber.e("Stop Discovery failed: Error code: $errorCode")
nsdManager?.stopServiceDiscovery(this)
}
}
}
// Instantiate DNS-SD resolve listener to get extra information about the service
private fun initializeResolveListener() {
resolveListener = object : NsdManager.ResolveListener {
override fun onServiceResolved(service: NsdServiceInfo) {
Timber.d("Resolve Succeeded: $service")
// Register the newly resolved service into our list of resolved services
resolvedNsdServices.add(service)
// Process the newly resolved service
onNsdServiceResolved(service)
// Process the next service waiting to be resolved
resolveNextInQueue()
}
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Called when the resolve fails. Use the error code to debug.
Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")
// Process the next service waiting to be resolved
resolveNextInQueue()
}
}
}
// Start discovering services on the network
fun discoverServices() {
// Cancel any existing discovery request
stopDiscovery()
initializeDiscoveryListener()
// Start looking for available audio channels in the network
nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
}
// Stop DNS-SD service discovery
fun stopDiscovery() {
if (discoveryListener != null) {
try {
nsdManager?.stopServiceDiscovery(discoveryListener)
} finally {
}
discoveryListener = null
}
}
// Resolve next NSD service pending resolution
private fun resolveNextInQueue() {
// Get the next NSD service waiting to be resolved from the queue
val nextNsdService = pendingNsdServices.poll()
if (nextNsdService != null) {
// There was one. Send to be resolved.
nsdManager?.resolveService(nextNsdService, resolveListener)
}
else {
// There was no pending service. Release the flag
resolveListenerBusy.set(false)
}
}
// Function to be overriden with custom logic for new service resolved
abstract fun onNsdServiceResolved(service: NsdServiceInfo)
// Function to be overriden with custom logic for service lost
abstract fun onNsdServiceLost(service: NsdServiceInfo)
}
And this is how to use it from a ViewModel (or from an activity or fragment, if you change from where to invoke the different helper methods):
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.nsd.NsdServiceInfo
import androidx.lifecycle.AndroidViewModel
import timber.log.Timber
import java.util.*
class MyViewModel(application: Application) : AndroidViewModel(application) {
// Get application context
private val myAppContext: Context = getApplication<Application>().applicationContext
// Declare NsdHelper object for service discovery
private val nsdHelper: NsdHelper? = object : NsdHelper(myAppContext) {
override fun onNsdServiceResolved(service: NsdServiceInfo) {
// A new network service is available
// Put your custom logic here!!!
}
override fun onNsdServiceLost(service: NsdServiceInfo) {
// A network service is no longer available
// Put your custom logic here!!!
}
}
// Block that is run when the view model is created
init {
// Initialize DNS-SD service discovery
nsdHelper?.initializeNsd()
// Start looking for available audio channels in the network
nsdHelper?.discoverServices()
}
// Called when the view model is destroyed
override fun onCleared() {
nsdHelper?.stopDiscovery()
Timber.d("onCleared called")
super.onCleared()
}
}