mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-02 15:56:24 +00:00
Use NsdManager for mDNS discovery on Android 14
This commit is contained in:
parent
1f72c82acb
commit
38588402e3
@ -5,11 +5,15 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
||||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
||||||
|
|
||||||
|
<!-- We don't need a MulticastLock on API level 34+ because we use NsdManager for mDNS -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||||
|
android:maxSdkVersion="33" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
@ -3,22 +3,21 @@ package com.limelight.discovery;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
|
import com.limelight.nvstream.mdns.JmDNSDiscoveryAgent;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
import com.limelight.nvstream.mdns.NsdManagerDiscoveryAgent;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.net.wifi.WifiManager.MulticastLock;
|
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
public class DiscoveryService extends Service {
|
public class DiscoveryService extends Service {
|
||||||
|
|
||||||
private MdnsDiscoveryAgent discoveryAgent;
|
private MdnsDiscoveryAgent discoveryAgent;
|
||||||
private MdnsDiscoveryListener boundListener;
|
private MdnsDiscoveryListener boundListener;
|
||||||
private MulticastLock multicastLock;
|
|
||||||
|
|
||||||
public class DiscoveryBinder extends Binder {
|
public class DiscoveryBinder extends Binder {
|
||||||
public void setListener(MdnsDiscoveryListener listener) {
|
public void setListener(MdnsDiscoveryListener listener) {
|
||||||
@ -26,13 +25,11 @@ public class DiscoveryService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void startDiscovery(int queryIntervalMs) {
|
public void startDiscovery(int queryIntervalMs) {
|
||||||
multicastLock.acquire();
|
|
||||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopDiscovery() {
|
public void stopDiscovery() {
|
||||||
discoveryAgent.stopDiscovery();
|
discoveryAgent.stopDiscovery();
|
||||||
multicastLock.release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MdnsComputer> getComputerSet() {
|
public List<MdnsComputer> getComputerSet() {
|
||||||
@ -42,11 +39,7 @@ public class DiscoveryService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
MdnsDiscoveryListener listener = new MdnsDiscoveryListener() {
|
||||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
|
||||||
multicastLock.setReferenceCounted(false);
|
|
||||||
|
|
||||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
if (boundListener != null) {
|
if (boundListener != null) {
|
||||||
@ -60,7 +53,22 @@ public class DiscoveryService extends Service {
|
|||||||
boundListener.notifyDiscoveryFailure(e);
|
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();
|
private final DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
@ -74,7 +82,6 @@ public class DiscoveryService extends Service {
|
|||||||
public boolean onUnbind(Intent intent) {
|
public boolean onUnbind(Intent intent) {
|
||||||
// Stop any discovery session
|
// Stop any discovery session
|
||||||
discoveryAgent.stopDiscovery();
|
discoveryAgent.stopDiscovery();
|
||||||
multicastLock.release();
|
|
||||||
|
|
||||||
// Unbind the listener
|
// Unbind the listener
|
||||||
boundListener = null;
|
boundListener = null;
|
||||||
|
@ -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<String> 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<ServiceListener> listeners = new HashSet<>();
|
||||||
|
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||||
|
@Override
|
||||||
|
public void serviceAdded(ServiceEvent event) {
|
||||||
|
HashSet<ServiceListener> 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<ServiceListener>(listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ServiceListener listener : localListeners) {
|
||||||
|
listener.serviceAdded(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serviceRemoved(ServiceEvent event) {
|
||||||
|
HashSet<ServiceListener> 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<ServiceListener>(listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ServiceListener listener : localListeners) {
|
||||||
|
listener.serviceRemoved(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serviceResolved(ServiceEvent event) {
|
||||||
|
HashSet<ServiceListener> 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<ServiceListener>(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<String> pendingNames;
|
||||||
|
synchronized (pendingResolution) {
|
||||||
|
pendingNames = new ArrayList<String>(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
|
||||||
|
}
|
||||||
|
}
|
@ -1,165 +1,64 @@
|
|||||||
package com.limelight.nvstream.mdns;
|
package com.limelight.nvstream.mdns;
|
||||||
|
|
||||||
import java.io.IOException;
|
import com.limelight.LimeLog;
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.Inet4Address;
|
import java.net.Inet4Address;
|
||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.jmdns.JmmDNS;
|
public abstract class MdnsDiscoveryAgent {
|
||||||
import javax.jmdns.NetworkTopologyDiscovery;
|
protected MdnsDiscoveryListener listener;
|
||||||
import javax.jmdns.ServiceEvent;
|
|
||||||
import javax.jmdns.ServiceInfo;
|
|
||||||
import javax.jmdns.ServiceListener;
|
|
||||||
import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
protected HashSet<MdnsComputer> computers = new HashSet<>();
|
||||||
|
|
||||||
public class MdnsDiscoveryAgent implements ServiceListener {
|
|
||||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
|
||||||
|
|
||||||
private MdnsDiscoveryListener listener;
|
|
||||||
private Thread discoveryThread;
|
|
||||||
private HashSet<MdnsComputer> computers = new HashSet<>();
|
|
||||||
private HashSet<String> 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<ServiceListener> listeners = new HashSet<>();
|
|
||||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
|
||||||
@Override
|
|
||||||
public void serviceAdded(ServiceEvent event) {
|
|
||||||
HashSet<ServiceListener> 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<ServiceListener>(listeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ServiceListener listener : localListeners) {
|
|
||||||
listener.serviceAdded(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void serviceRemoved(ServiceEvent event) {
|
|
||||||
HashSet<ServiceListener> 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<ServiceListener>(listeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ServiceListener listener : localListeners) {
|
|
||||||
listener.serviceRemoved(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void serviceResolved(ServiceEvent event) {
|
|
||||||
HashSet<ServiceListener> 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<ServiceListener>(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) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
public abstract void startDiscovery(final int discoveryIntervalMs);
|
||||||
synchronized (pendingResolution) {
|
|
||||||
pendingResolution.remove(info.getName());
|
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 {
|
// If there were no IPv4 addresses, use IPv6 for registration
|
||||||
handleServiceInfo(info);
|
if (v4Addrs.length == 0) {
|
||||||
} catch (UnsupportedEncodingException e) {
|
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||||
// Invalid DNS response
|
|
||||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||||
return;
|
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<MdnsComputer> getComputerSet() {
|
||||||
|
synchronized (computers) {
|
||||||
|
return new ArrayList<>(computers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||||
for (Inet6Address addr : addresses) {
|
for (Inet6Address addr : addresses) {
|
||||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||||
return addr;
|
return addr;
|
||||||
@ -173,7 +72,7 @@ public class MdnsDiscoveryAgent implements ServiceListener {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
protected static Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||||
for (Inet6Address addr : addresses) {
|
for (Inet6Address addr : addresses) {
|
||||||
if (addr.isLinkLocalAddress()) {
|
if (addr.isLinkLocalAddress()) {
|
||||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||||
@ -184,7 +83,7 @@ public class MdnsDiscoveryAgent implements ServiceListener {
|
|||||||
return null;
|
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
|
// 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).
|
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||||
@ -246,139 +145,4 @@ public class MdnsDiscoveryAgent implements ServiceListener {
|
|||||||
|
|
||||||
return null;
|
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<String> pendingNames;
|
|
||||||
synchronized (pendingResolution) {
|
|
||||||
pendingNames = new ArrayList<String>(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<MdnsComputer> 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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<String, NsdManager.ServiceInfoCallback> 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<InetAddress> 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<InetAddress> 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user