diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 05682412..b301979c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,11 +5,15 @@
-
+
+
+
diff --git a/app/src/main/java/com/limelight/discovery/DiscoveryService.java b/app/src/main/java/com/limelight/discovery/DiscoveryService.java
index 07644ac1..f8c855d6 100644
--- a/app/src/main/java/com/limelight/discovery/DiscoveryService.java
+++ b/app/src/main/java/com/limelight/discovery/DiscoveryService.java
@@ -3,22 +3,21 @@ package com.limelight.discovery;
import java.util.List;
import com.limelight.nvstream.mdns.MdnsComputer;
+import com.limelight.nvstream.mdns.JmDNSDiscoveryAgent;
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
+import com.limelight.nvstream.mdns.NsdManagerDiscoveryAgent;
import android.app.Service;
-import android.content.Context;
import android.content.Intent;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.MulticastLock;
import android.os.Binder;
+import android.os.Build;
import android.os.IBinder;
public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
- private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
@@ -26,13 +25,11 @@ public class DiscoveryService extends Service {
}
public void startDiscovery(int queryIntervalMs) {
- multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
- multicastLock.release();
}
public List getComputerSet() {
@@ -42,11 +39,7 @@ public class DiscoveryService extends Service {
@Override
public void onCreate() {
- WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
- multicastLock.setReferenceCounted(false);
-
- discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
+ MdnsDiscoveryListener listener = new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
@@ -60,7 +53,22 @@ public class DiscoveryService extends Service {
boundListener.notifyDiscoveryFailure(e);
}
}
- });
+ };
+
+ // Prior to Android 14, NsdManager doesn't provide all the capabilities needed for parity
+ // with jmDNS (specifically handling multiple addresses for a single service). There are
+ // also documented reliability bugs early in the Android 4.x series shortly after it was
+ // introduced. The benefit of using NsdManager over jmDNS is that it works correctly in
+ // environments where mDNS proxying is required, like ChromeOS, WSA, and the emulator.
+ //
+ // As such, we use the jmDNS-based MdnsDiscoveryAgent prior to Android 14 and NsdManager
+ // on Android 14 and above.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ discoveryAgent = new JmDNSDiscoveryAgent(getApplicationContext(), listener);
+ }
+ else {
+ discoveryAgent = new NsdManagerDiscoveryAgent(getApplicationContext(), listener);
+ }
}
private final DiscoveryBinder binder = new DiscoveryBinder();
@@ -74,7 +82,6 @@ public class DiscoveryService extends Service {
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
- multicastLock.release();
// Unbind the listener
boundListener = null;
diff --git a/app/src/main/java/com/limelight/nvstream/mdns/JmDNSDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/JmDNSDiscoveryAgent.java
new file mode 100644
index 00000000..466304cf
--- /dev/null
+++ b/app/src/main/java/com/limelight/nvstream/mdns/JmDNSDiscoveryAgent.java
@@ -0,0 +1,269 @@
+package com.limelight.nvstream.mdns;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import javax.jmdns.JmmDNS;
+import javax.jmdns.NetworkTopologyDiscovery;
+import javax.jmdns.ServiceEvent;
+import javax.jmdns.ServiceInfo;
+import javax.jmdns.ServiceListener;
+import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
+
+import com.limelight.LimeLog;
+
+public class JmDNSDiscoveryAgent extends MdnsDiscoveryAgent implements ServiceListener {
+ private static final String SERVICE_TYPE = "_nvstream._tcp.local.";
+ private WifiManager.MulticastLock multicastLock;
+ private Thread discoveryThread;
+ private HashSet pendingResolution = new HashSet<>();
+
+ // The resolver factory's instance member has a static lifetime which
+ // means our ref count and listener must be static also.
+ private static int resolverRefCount = 0;
+ private static HashSet listeners = new HashSet<>();
+ private static ServiceListener nvstreamListener = new ServiceListener() {
+ @Override
+ public void serviceAdded(ServiceEvent event) {
+ HashSet localListeners;
+
+ // Copy the listener set into a new set so we can invoke
+ // the callbacks without holding the listeners monitor the
+ // whole time.
+ synchronized (listeners) {
+ localListeners = new HashSet(listeners);
+ }
+
+ for (ServiceListener listener : localListeners) {
+ listener.serviceAdded(event);
+ }
+ }
+
+ @Override
+ public void serviceRemoved(ServiceEvent event) {
+ HashSet localListeners;
+
+ // Copy the listener set into a new set so we can invoke
+ // the callbacks without holding the listeners monitor the
+ // whole time.
+ synchronized (listeners) {
+ localListeners = new HashSet(listeners);
+ }
+
+ for (ServiceListener listener : localListeners) {
+ listener.serviceRemoved(event);
+ }
+ }
+
+ @Override
+ public void serviceResolved(ServiceEvent event) {
+ HashSet localListeners;
+
+ // Copy the listener set into a new set so we can invoke
+ // the callbacks without holding the listeners monitor the
+ // whole time.
+ synchronized (listeners) {
+ localListeners = new HashSet(listeners);
+ }
+
+ for (ServiceListener listener : localListeners) {
+ listener.serviceResolved(event);
+ }
+ }
+ };
+
+ public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
+ @Override
+ public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
+ // This is an copy of jmDNS's implementation, except we omit the multicast check, since
+ // it seems at least some devices lie about interfaces not supporting multicast when they really do.
+ try {
+ if (!networkInterface.isUp()) {
+ return false;
+ }
+
+ /*
+ if (!networkInterface.supportsMulticast()) {
+ return false;
+ }
+ */
+
+ if (networkInterface.isLoopback()) {
+ return false;
+ }
+
+ return true;
+ } catch (Exception exception) {
+ return false;
+ }
+ }
+ }
+
+ static {
+ // Override jmDNS's default topology discovery class with ours
+ NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
+ @Override
+ public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
+ return new MyNetworkTopologyDiscovery();
+ }
+ });
+ }
+
+ private static JmmDNS referenceResolver() {
+ synchronized (JmDNSDiscoveryAgent.class) {
+ JmmDNS instance = JmmDNS.Factory.getInstance();
+ if (++resolverRefCount == 1) {
+ // This will cause the listener to be invoked for known hosts immediately.
+ // JmDNS only supports one listener per service, so we have to do this here
+ // with a static listener.
+ instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
+ }
+ return instance;
+ }
+ }
+
+ private static void dereferenceResolver() {
+ synchronized (JmDNSDiscoveryAgent.class) {
+ if (--resolverRefCount == 0) {
+ try {
+ JmmDNS.Factory.close();
+ } catch (IOException e) {}
+ }
+ }
+ }
+
+ public JmDNSDiscoveryAgent(Context context, MdnsDiscoveryListener listener) {
+ super(listener);
+
+ // Create the multicast lock required to receive mDNS traffic
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
+ multicastLock.setReferenceCounted(false);
+ }
+
+ private void handleResolvedServiceInfo(ServiceInfo info) {
+ synchronized (pendingResolution) {
+ pendingResolution.remove(info.getName());
+ }
+
+ try {
+ handleServiceInfo(info);
+ } catch (UnsupportedEncodingException e) {
+ // Invalid DNS response
+ LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
+ return;
+ }
+ }
+
+ private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
+ reportNewComputer(info.getName(), info.getPort(), info.getInet4Addresses(), info.getInet6Addresses());
+ }
+
+ public void startDiscovery(final int discoveryIntervalMs) {
+ // Kill any existing discovery before starting a new one
+ stopDiscovery();
+
+ // Acquire the multicast lock to start receiving mDNS traffic
+ multicastLock.acquire();
+
+ // Add our listener to the set
+ synchronized (listeners) {
+ listeners.add(JmDNSDiscoveryAgent.this);
+ }
+
+ discoveryThread = new Thread() {
+ @Override
+ public void run() {
+ // This may result in listener callbacks so we must register
+ // our listener first.
+ JmmDNS resolver = referenceResolver();
+
+ try {
+ while (!Thread.interrupted()) {
+ // Start an mDNS request
+ resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
+
+ // Run service resolution again for pending machines
+ ArrayList pendingNames;
+ synchronized (pendingResolution) {
+ pendingNames = new ArrayList(pendingResolution);
+ }
+ for (String name : pendingNames) {
+ LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
+ ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
+ if (infos != null && infos.length != 0) {
+ LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
+ for (ServiceInfo svcinfo : infos) {
+ handleResolvedServiceInfo(svcinfo);
+ }
+ }
+ }
+
+ // Wait for the next polling interval
+ try {
+ Thread.sleep(discoveryIntervalMs);
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ }
+ finally {
+ // Dereference the resolver
+ dereferenceResolver();
+ }
+ }
+ };
+ discoveryThread.setName("mDNS Discovery Thread");
+ discoveryThread.start();
+ }
+
+ public void stopDiscovery() {
+ // Release the multicast lock to stop receiving mDNS traffic
+ multicastLock.release();
+
+ // Remove our listener from the set
+ synchronized (listeners) {
+ listeners.remove(JmDNSDiscoveryAgent.this);
+ }
+
+ // If there's already a running thread, interrupt it
+ if (discoveryThread != null) {
+ discoveryThread.interrupt();
+ discoveryThread = null;
+ }
+ }
+
+ @Override
+ public void serviceAdded(ServiceEvent event) {
+ LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
+
+ ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
+ if (info == null) {
+ // This machine is pending resolution
+ synchronized (pendingResolution) {
+ pendingResolution.add(event.getInfo().getName());
+ }
+ return;
+ }
+
+ LimeLog.info("mDNS: Resolved (blocking)");
+ handleResolvedServiceInfo(info);
+ }
+
+ @Override
+ public void serviceRemoved(ServiceEvent event) {
+ LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
+ }
+
+ @Override
+ public void serviceResolved(ServiceEvent event) {
+ // We handle this synchronously
+ }
+}
diff --git a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java
index b242297a..661578b6 100644
--- a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java
+++ b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java
@@ -1,165 +1,64 @@
package com.limelight.nvstream.mdns;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import com.limelight.LimeLog;
+
import java.net.Inet4Address;
import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
-import javax.jmdns.JmmDNS;
-import javax.jmdns.NetworkTopologyDiscovery;
-import javax.jmdns.ServiceEvent;
-import javax.jmdns.ServiceInfo;
-import javax.jmdns.ServiceListener;
-import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
+public abstract class MdnsDiscoveryAgent {
+ protected MdnsDiscoveryListener listener;
-import com.limelight.LimeLog;
-
-public class MdnsDiscoveryAgent implements ServiceListener {
- public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
-
- private MdnsDiscoveryListener listener;
- private Thread discoveryThread;
- private HashSet computers = new HashSet<>();
- private HashSet pendingResolution = new HashSet<>();
-
- // The resolver factory's instance member has a static lifetime which
- // means our ref count and listener must be static also.
- private static int resolverRefCount = 0;
- private static HashSet listeners = new HashSet<>();
- private static ServiceListener nvstreamListener = new ServiceListener() {
- @Override
- public void serviceAdded(ServiceEvent event) {
- HashSet localListeners;
-
- // Copy the listener set into a new set so we can invoke
- // the callbacks without holding the listeners monitor the
- // whole time.
- synchronized (listeners) {
- localListeners = new HashSet(listeners);
- }
-
- for (ServiceListener listener : localListeners) {
- listener.serviceAdded(event);
- }
- }
-
- @Override
- public void serviceRemoved(ServiceEvent event) {
- HashSet localListeners;
-
- // Copy the listener set into a new set so we can invoke
- // the callbacks without holding the listeners monitor the
- // whole time.
- synchronized (listeners) {
- localListeners = new HashSet(listeners);
- }
-
- for (ServiceListener listener : localListeners) {
- listener.serviceRemoved(event);
- }
- }
-
- @Override
- public void serviceResolved(ServiceEvent event) {
- HashSet localListeners;
-
- // Copy the listener set into a new set so we can invoke
- // the callbacks without holding the listeners monitor the
- // whole time.
- synchronized (listeners) {
- localListeners = new HashSet(listeners);
- }
-
- for (ServiceListener listener : localListeners) {
- listener.serviceResolved(event);
- }
- }
- };
-
- public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
- @Override
- public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
- // This is an copy of jmDNS's implementation, except we omit the multicast check, since
- // it seems at least some devices lie about interfaces not supporting multicast when they really do.
- try {
- if (!networkInterface.isUp()) {
- return false;
- }
-
- /*
- if (!networkInterface.supportsMulticast()) {
- return false;
- }
- */
-
- if (networkInterface.isLoopback()) {
- return false;
- }
-
- return true;
- } catch (Exception exception) {
- return false;
- }
- }
- }
-
- static {
- // Override jmDNS's default topology discovery class with ours
- NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
- @Override
- public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
- return new MyNetworkTopologyDiscovery();
- }
- });
- }
-
- private static JmmDNS referenceResolver() {
- synchronized (MdnsDiscoveryAgent.class) {
- JmmDNS instance = JmmDNS.Factory.getInstance();
- if (++resolverRefCount == 1) {
- // This will cause the listener to be invoked for known hosts immediately.
- // JmDNS only supports one listener per service, so we have to do this here
- // with a static listener.
- instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
- }
- return instance;
- }
- }
-
- private static void dereferenceResolver() {
- synchronized (MdnsDiscoveryAgent.class) {
- if (--resolverRefCount == 0) {
- try {
- JmmDNS.Factory.close();
- } catch (IOException e) {}
- }
- }
- }
+ protected HashSet computers = new HashSet<>();
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
this.listener = listener;
}
- private void handleResolvedServiceInfo(ServiceInfo info) {
- synchronized (pendingResolution) {
- pendingResolution.remove(info.getName());
+ public abstract void startDiscovery(final int discoveryIntervalMs);
+
+ public abstract void stopDiscovery();
+
+ protected void reportNewComputer(String name, int port, Inet4Address[] v4Addrs, Inet6Address[] v6Addrs) {
+ LimeLog.info("mDNS: "+name+" has "+v4Addrs.length+" IPv4 addresses");
+ LimeLog.info("mDNS: "+name+" has "+v6Addrs.length+" IPv6 addresses");
+
+ Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
+
+ // Add a computer object for each IPv4 address reported by the PC
+ for (Inet4Address v4Addr : v4Addrs) {
+ synchronized (computers) {
+ MdnsComputer computer = new MdnsComputer(name, v4Addr, v6GlobalAddr, port);
+ if (computers.add(computer)) {
+ // This was a new entry
+ listener.notifyComputerAdded(computer);
+ }
+ }
}
- try {
- handleServiceInfo(info);
- } catch (UnsupportedEncodingException e) {
- // Invalid DNS response
- LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
- return;
+ // If there were no IPv4 addresses, use IPv6 for registration
+ if (v4Addrs.length == 0) {
+ Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
+
+ if (v6LocalAddr != null || v6GlobalAddr != null) {
+ MdnsComputer computer = new MdnsComputer(name, v6LocalAddr, v6GlobalAddr, port);
+ if (computers.add(computer)) {
+ // This was a new entry
+ listener.notifyComputerAdded(computer);
+ }
+ }
}
}
- private Inet6Address getLocalAddress(Inet6Address[] addresses) {
+ public List getComputerSet() {
+ synchronized (computers) {
+ return new ArrayList<>(computers);
+ }
+ }
+
+ protected static Inet6Address getLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
return addr;
@@ -173,7 +72,7 @@ public class MdnsDiscoveryAgent implements ServiceListener {
return null;
}
- private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
+ protected static Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress()) {
LimeLog.info("Found link-local address: "+addr.getHostAddress());
@@ -184,7 +83,7 @@ public class MdnsDiscoveryAgent implements ServiceListener {
return null;
}
- private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
+ protected static Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
// First try to find a link local address, so we can match the interface identifier
// with a global address (this will work for SLAAC but not DHCPv6).
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
@@ -246,139 +145,4 @@ public class MdnsDiscoveryAgent implements ServiceListener {
return null;
}
-
- private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
- Inet4Address v4Addrs[] = info.getInet4Addresses();
- Inet6Address v6Addrs[] = info.getInet6Addresses();
-
- LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
- LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
-
- Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
-
- // Add a computer object for each IPv4 address reported by the PC
- for (Inet4Address v4Addr : v4Addrs) {
- synchronized (computers) {
- MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr, info.getPort());
- if (computers.add(computer)) {
- // This was a new entry
- listener.notifyComputerAdded(computer);
- }
- }
- }
-
- // If there were no IPv4 addresses, use IPv6 for registration
- if (v4Addrs.length == 0) {
- Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
-
- if (v6LocalAddr != null || v6GlobalAddr != null) {
- MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr, info.getPort());
- if (computers.add(computer)) {
- // This was a new entry
- listener.notifyComputerAdded(computer);
- }
- }
- }
- }
-
- public void startDiscovery(final int discoveryIntervalMs) {
- // Kill any existing discovery before starting a new one
- stopDiscovery();
-
- // Add our listener to the set
- synchronized (listeners) {
- listeners.add(MdnsDiscoveryAgent.this);
- }
-
- discoveryThread = new Thread() {
- @Override
- public void run() {
- // This may result in listener callbacks so we must register
- // our listener first.
- JmmDNS resolver = referenceResolver();
-
- try {
- while (!Thread.interrupted()) {
- // Start an mDNS request
- resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
-
- // Run service resolution again for pending machines
- ArrayList pendingNames;
- synchronized (pendingResolution) {
- pendingNames = new ArrayList(pendingResolution);
- }
- for (String name : pendingNames) {
- LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
- ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
- if (infos != null && infos.length != 0) {
- LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
- for (ServiceInfo svcinfo : infos) {
- handleResolvedServiceInfo(svcinfo);
- }
- }
- }
-
- // Wait for the next polling interval
- try {
- Thread.sleep(discoveryIntervalMs);
- } catch (InterruptedException e) {
- break;
- }
- }
- }
- finally {
- // Dereference the resolver
- dereferenceResolver();
- }
- }
- };
- discoveryThread.setName("mDNS Discovery Thread");
- discoveryThread.start();
- }
-
- public void stopDiscovery() {
- // Remove our listener from the set
- synchronized (listeners) {
- listeners.remove(MdnsDiscoveryAgent.this);
- }
-
- // If there's already a running thread, interrupt it
- if (discoveryThread != null) {
- discoveryThread.interrupt();
- discoveryThread = null;
- }
- }
-
- public List getComputerSet() {
- synchronized (computers) {
- return new ArrayList<>(computers);
- }
- }
-
- @Override
- public void serviceAdded(ServiceEvent event) {
- LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
-
- ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
- if (info == null) {
- // This machine is pending resolution
- synchronized (pendingResolution) {
- pendingResolution.add(event.getInfo().getName());
- }
- return;
- }
-
- LimeLog.info("mDNS: Resolved (blocking)");
- handleResolvedServiceInfo(info);
- }
-
- @Override
- public void serviceRemoved(ServiceEvent event) {
- LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
- }
-
- @Override
- public void serviceResolved(ServiceEvent event) {
- // We handle this synchronously
- }
}
diff --git a/app/src/main/java/com/limelight/nvstream/mdns/NsdManagerDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/NsdManagerDiscoveryAgent.java
new file mode 100644
index 00000000..f31060b2
--- /dev/null
+++ b/app/src/main/java/com/limelight/nvstream/mdns/NsdManagerDiscoveryAgent.java
@@ -0,0 +1,175 @@
+package com.limelight.nvstream.mdns;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Build;
+
+import com.limelight.LimeLog;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class NsdManagerDiscoveryAgent extends MdnsDiscoveryAgent {
+ private static final String SERVICE_TYPE = "_nvstream._tcp";
+ private NsdManager nsdManager;
+ private boolean discoveryActive;
+ private boolean wantsDiscoveryActive;
+ private final HashMap serviceCallbacks = new HashMap<>();
+ private final ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+
+ private NsdManager.DiscoveryListener discoveryListener = new NsdManager.DiscoveryListener() {
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ discoveryActive = false;
+ LimeLog.severe("NSD: Service discovery start failed: " + errorCode);
+ listener.notifyDiscoveryFailure(new RuntimeException("onStartDiscoveryFailed(): " + errorCode));
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ LimeLog.severe("NSD: Service discovery stop failed: " + errorCode);
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ discoveryActive = true;
+ LimeLog.info("NSD: Service discovery started");
+
+ // If we were stopped before we could finish starting, stop now
+ if (!wantsDiscoveryActive) {
+ stopDiscovery();
+ }
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ discoveryActive = false;
+ LimeLog.info("NSD: Service discovery stopped");
+
+ // If we were started before we could finish stopping, start again now
+ if (wantsDiscoveryActive) {
+ startDiscovery(0);
+ }
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo nsdServiceInfo) {
+ LimeLog.info("NSD: Machine appeared: "+nsdServiceInfo.getServiceName());
+
+ NsdManager.ServiceInfoCallback serviceInfoCallback = new NsdManager.ServiceInfoCallback() {
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
+ LimeLog.severe("NSD: Service info callback registration failed: " + errorCode);
+ listener.notifyDiscoveryFailure(new RuntimeException("onServiceInfoCallbackRegistrationFailed(): " + errorCode));
+ }
+
+ @Override
+ public void onServiceUpdated(NsdServiceInfo nsdServiceInfo) {
+ LimeLog.info("NSD: Machine resolved: "+nsdServiceInfo.getServiceName());
+ reportNewComputer(nsdServiceInfo.getServiceName(), nsdServiceInfo.getPort(),
+ getV4Addrs(nsdServiceInfo.getHostAddresses()),
+ getV6Addrs(nsdServiceInfo.getHostAddresses()));
+ }
+
+ @Override
+ public void onServiceLost() {}
+
+ @Override
+ public void onServiceInfoCallbackUnregistered() {}
+ };
+
+ nsdManager.registerServiceInfoCallback(nsdServiceInfo, executor, serviceInfoCallback);
+ serviceCallbacks.put(nsdServiceInfo.getServiceName(), serviceInfoCallback);
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo nsdServiceInfo) {
+ LimeLog.info("NSD: Machine lost: "+nsdServiceInfo.getServiceName());
+
+ NsdManager.ServiceInfoCallback serviceInfoCallback = serviceCallbacks.remove(nsdServiceInfo.getServiceName());
+ if (serviceInfoCallback != null) {
+ nsdManager.unregisterServiceInfoCallback(serviceInfoCallback);
+ }
+ }
+ };
+
+ public NsdManagerDiscoveryAgent(Context context, MdnsDiscoveryListener listener) {
+ super(listener);
+ this.nsdManager = context.getSystemService(NsdManager.class);
+ }
+
+ @Override
+ public void startDiscovery(int discoveryIntervalMs) {
+ wantsDiscoveryActive = true;
+
+ // Register the service discovery listener
+ if (!discoveryActive) {
+ nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
+ }
+ }
+
+ @Override
+ public void stopDiscovery() {
+ wantsDiscoveryActive = false;
+
+ // Unregister the service discovery listener
+ if (discoveryActive) {
+ nsdManager.stopServiceDiscovery(discoveryListener);
+ }
+
+ // Unregister all service info callbacks
+ for (NsdManager.ServiceInfoCallback callback : serviceCallbacks.values()) {
+ nsdManager.unregisterServiceInfoCallback(callback);
+ }
+ serviceCallbacks.clear();
+ }
+
+ private static Inet4Address[] getV4Addrs(List addrs) {
+ int matchCount = 0;
+ for (InetAddress addr : addrs) {
+ if (addr instanceof Inet4Address) {
+ matchCount++;
+ }
+ }
+
+ Inet4Address[] matching = new Inet4Address[matchCount];
+
+ int i = 0;
+ for (InetAddress addr : addrs) {
+ if (addr instanceof Inet4Address) {
+ matching[i++] = (Inet4Address) addr;
+ }
+ }
+
+ return matching;
+ }
+
+ private static Inet6Address[] getV6Addrs(List addrs) {
+ int matchCount = 0;
+ for (InetAddress addr : addrs) {
+ if (addr instanceof Inet6Address) {
+ matchCount++;
+ }
+ }
+
+ Inet6Address[] matching = new Inet6Address[matchCount];
+
+ int i = 0;
+ for (InetAddress addr : addrs) {
+ if (addr instanceof Inet6Address) {
+ matching[i++] = (Inet6Address) addr;
+ }
+ }
+
+ return matching;
+ }
+}