diff --git a/moonlight-common/src/com/limelight/nvstream/NvConnection.java b/moonlight-common/src/com/limelight/nvstream/NvConnection.java index 2ff978bb..375de1a9 100644 --- a/moonlight-common/src/com/limelight/nvstream/NvConnection.java +++ b/moonlight-common/src/com/limelight/nvstream/NvConnection.java @@ -35,6 +35,7 @@ public class NvConnection { private NvConnectionListener listener; private StreamConfiguration config; private LimelightCryptoProvider cryptoProvider; + private String uniqueId; private InetAddress hostAddr; private ControlStream controlStream; @@ -52,12 +53,13 @@ public class NvConnection { private ThreadPoolExecutor threadPool; - public NvConnection(String host, NvConnectionListener listener, StreamConfiguration config, LimelightCryptoProvider cryptoProvider) + public NvConnection(String host, String uniqueId, NvConnectionListener listener, StreamConfiguration config, LimelightCryptoProvider cryptoProvider) { this.host = host; this.listener = listener; this.config = config; this.cryptoProvider = cryptoProvider; + this.uniqueId = uniqueId; try { // This is unique per connection @@ -162,7 +164,7 @@ public class NvConnection { private boolean startApp() throws XmlPullParserException, IOException { - NvHTTP h = new NvHTTP(hostAddr, getMacAddressString(), localDeviceName, cryptoProvider); + NvHTTP h = new NvHTTP(hostAddr, uniqueId, localDeviceName, cryptoProvider); if (h.getServerVersion().startsWith("1.")) { listener.displayMessage("Limelight now requires GeForce Experience 2.0.1 or later. Please upgrade GFE on your PC and try again."); diff --git a/moonlight-common/src/com/limelight/nvstream/http/ComputerDetails.java b/moonlight-common/src/com/limelight/nvstream/http/ComputerDetails.java new file mode 100644 index 00000000..aea24521 --- /dev/null +++ b/moonlight-common/src/com/limelight/nvstream/http/ComputerDetails.java @@ -0,0 +1,68 @@ +package com.limelight.nvstream.http; + +import java.net.InetAddress; +import java.util.UUID; + + +public class ComputerDetails { + public enum State { + ONLINE, OFFLINE, UNKNOWN + } + public enum Reachability { + LOCAL, REMOTE, OFFLINE, UNKNOWN + } + + public State state; + public Reachability reachability; + public String name; + public UUID uuid; + public InetAddress localIp; + public InetAddress remoteIp; + public PairingManager.PairState pairState; + public String macAddress; + public int runningGameId; + + public void update(ComputerDetails details) { + this.state = details.state; + this.name = details.name; + this.uuid = details.uuid; + this.localIp = details.localIp; + this.remoteIp = details.remoteIp; + this.macAddress = details.macAddress; + this.pairState = details.pairState; + this.runningGameId = details.runningGameId; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ComputerDetails) { + ComputerDetails other = (ComputerDetails)o; + + // Use UUIDs if they both have them + if (other.uuid != null && this.uuid != null) + { + return other.uuid.equals(this.uuid); + } + + // Otherwise use local IP + return other.localIp.equals(this.localIp); + } + + return false; + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + str.append("State: ").append(state).append("\n"); + str.append("Reachability: ").append(reachability).append("\n"); + str.append("Name: ").append(name).append("\n"); + str.append("UUID: ").append(uuid).append("\n"); + str.append("Local IP: ").append(localIp).append("\n"); + str.append("Remote IP: ").append(remoteIp).append("\n"); + str.append("MAC Address: ").append(macAddress).append("\n"); + str.append("Pair State: ").append(pairState).append("\n"); + str.append("Running Game ID: ").append(runningGameId).append("\n"); + return str.toString(); + } +} diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index 6204dff2..a0e8d9b5 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -13,6 +13,7 @@ import java.net.URLConnection; import java.util.LinkedList; import java.util.Scanner; import java.util.Stack; +import java.util.UUID; import javax.crypto.SecretKey; @@ -20,6 +21,8 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; +import com.limelight.nvstream.http.PairingManager.PairState; + public class NvHTTP { private String uniqueId; @@ -27,7 +30,7 @@ public class NvHTTP { private LimelightCryptoProvider cryptoProvider; public static final int PORT = 47984; - public static final int CONNECTION_TIMEOUT = 5000; + public static final int CONNECTION_TIMEOUT = 2000; private final boolean verbose = false; @@ -96,6 +99,35 @@ public class NvHTTP { throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message")); } } + + public ComputerDetails getComputerDetails() throws MalformedURLException, IOException, XmlPullParserException { + ComputerDetails details = new ComputerDetails(); + String serverInfo = openHttpConnectionToString(baseUrl + "/serverinfo?uniqueid=" + uniqueId); + + details.name = getXmlString(serverInfo, "hostname"); + details.uuid = UUID.fromString(getXmlString(serverInfo, "uniqueid")); + details.localIp = InetAddress.getByName(getXmlString(serverInfo, "LocalIP")); + details.remoteIp = InetAddress.getByName(getXmlString(serverInfo, "ExternalIP")); + details.macAddress = getXmlString(serverInfo, "mac"); + + try { + details.pairState = Integer.parseInt(getXmlString(serverInfo, "PairStatus")) == 1 ? + PairState.PAIRED : PairState.NOT_PAIRED; + } catch (NumberFormatException e) { + details.pairState = PairState.FAILED; + } + + try { + details.runningGameId = Integer.parseInt(getXmlString(serverInfo, "currentgame")); + } catch (NumberFormatException e) { + details.runningGameId = 0; + } + + // We could reach it so it's online + details.state = ComputerDetails.State.ONLINE; + + return details; + } private InputStream openHttpConnection(String url) throws IOException { URLConnection conn = new URL(url).openConnection(); @@ -200,6 +232,10 @@ public class NvHTTP { } return appList; } + + public void unpair() throws IOException { + openHttpConnection(baseUrl + "/unpair?uniqueid=" + uniqueId); + } public int launchApp(int appId, int width, int height, int refreshRate, SecretKey inputKey) throws IOException, XmlPullParserException { InputStream in = openHttpConnection(baseUrl + diff --git a/moonlight-common/src/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java b/moonlight-common/src/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java index 56ea11f2..a5e70571 100644 --- a/moonlight-common/src/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java +++ b/moonlight-common/src/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.Inet4Address; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -127,7 +128,6 @@ public class MdnsDiscoveryAgent { private MdnsDiscoveryAgent(MdnsDiscoveryListener listener) { computers = new HashMap(); - discoveryTimer = new Timer(); pendingResolution = new ArrayList(); this.listener = listener; } @@ -135,7 +135,7 @@ public class MdnsDiscoveryAgent { public static MdnsDiscoveryAgent createDiscoveryAgent(MdnsDiscoveryListener listener) throws IOException { MdnsDiscoveryAgent agent = new MdnsDiscoveryAgent(listener); - agent.resolver = JmDNS.create(); + agent.resolver = JmDNS.create(new InetSocketAddress(0).getAddress()); return agent; } @@ -143,12 +143,13 @@ public class MdnsDiscoveryAgent { public void startDiscovery(final int discoveryIntervalMs) { resolver.addServiceListener(SERVICE_TYPE, nvstreamListener); + discoveryTimer = new Timer(); discoveryTimer.schedule(new TimerTask() { @Override public void run() { // Send another mDNS query resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs); - + // Run service resolution again for pending machines ArrayList pendingNames = new ArrayList(pendingResolution); for (String name : pendingNames) { @@ -162,7 +163,10 @@ public class MdnsDiscoveryAgent { public void stopDiscovery() { resolver.removeServiceListener(SERVICE_TYPE, nvstreamListener); - discoveryTimer.cancel(); + if (discoveryTimer != null) { + discoveryTimer.cancel(); + discoveryTimer = null; + } } public List getComputerSet() { diff --git a/moonlight-common/src/com/limelight/nvstream/wol/WakeOnLanSender.java b/moonlight-common/src/com/limelight/nvstream/wol/WakeOnLanSender.java new file mode 100644 index 00000000..312bfc4f --- /dev/null +++ b/moonlight-common/src/com/limelight/nvstream/wol/WakeOnLanSender.java @@ -0,0 +1,72 @@ +package com.limelight.nvstream.wol; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.Scanner; + +import com.limelight.nvstream.http.ComputerDetails; + +public class WakeOnLanSender { + private static final int[] PORTS_TO_TRY = new int[] { + 7, 9, // Standard WOL ports + 47998, 47999, 48000 // Ports opened by GFE + }; + + public static void sendWolPacket(ComputerDetails computer) throws IOException { + DatagramSocket sock = new DatagramSocket(0); + + byte[] payload = createWolPayload(computer); + // Try both remote and local addresses + for (int i = 0; i < 2; i++) { + InetAddress addr; + if (i == 0) { + addr = computer.localIp; + } + else { + addr = computer.remoteIp; + } + + // Try all the ports for each address + for (int port : PORTS_TO_TRY) { + DatagramPacket dp = new DatagramPacket(payload, payload.length); + dp.setAddress(addr); + dp.setPort(port); + sock.send(dp); + } + } + + sock.close(); + } + + private static byte[] macStringToBytes(String macAddress) { + byte[] macBytes = new byte[6]; + @SuppressWarnings("resource") + Scanner scan = new Scanner(macAddress).useDelimiter(":"); + for (int i = 0; i < macBytes.length && scan.hasNext(); i++) { + macBytes[i] = (byte) Integer.parseInt(scan.next(), 16); + } + scan.close(); + return macBytes; + } + + private static byte[] createWolPayload(ComputerDetails computer) { + byte[] payload = new byte[102]; + byte[] macAddress = macStringToBytes(computer.macAddress); + int i; + + // 6 bytes of FF + for (i = 0; i < 6; i++) { + payload[i] = (byte)0xFF; + } + + // 16 repetitions of the MAC address + for (int j = 0; j < 16; j++) { + System.arraycopy(macAddress, 0, payload, i, macAddress.length); + i += macAddress.length; + } + + return payload; + } +}