diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 5cff3e6d..d4c9df2c 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -470,6 +470,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, .setAudioEncryption(true) .setColorSpace(decoderRenderer.getPreferredColorSpace()) .setColorRange(decoderRenderer.getPreferredColorRange()) + .setPersistGamepadsAfterDisconnect(!prefConfig.multiController) .build(); // Initialize the connection diff --git a/app/src/main/java/com/limelight/nvstream/ConnectionContext.java b/app/src/main/java/com/limelight/nvstream/ConnectionContext.java index af6d0b57..17d916ff 100644 --- a/app/src/main/java/com/limelight/nvstream/ConnectionContext.java +++ b/app/src/main/java/com/limelight/nvstream/ConnectionContext.java @@ -9,6 +9,7 @@ import javax.crypto.SecretKey; public class ConnectionContext { public ComputerDetails.AddressTuple serverAddress; public int httpsPort; + public boolean isNvidiaServerSoftware; public X509Certificate serverCert; public StreamConfiguration streamConfig; public NvConnectionListener connListener; diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index 0b799dd8..6792c3a8 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -231,6 +231,9 @@ public class NvConnection { return false; } + ComputerDetails details = h.getComputerDetails(serverInfo); + context.isNvidiaServerSoftware = details.nvidiaServer; + // May be missing for older servers context.serverGfeVersion = h.getGfeVersion(serverInfo); @@ -306,7 +309,7 @@ public class NvConnection { if (h.getCurrentGame(serverInfo) != 0) { try { if (h.getCurrentGame(serverInfo) == app.getAppId()) { - if (!h.resumeApp(context)) { + if (!h.launchApp(context, "resume", app.getAppId(), context.negotiatedHdr)) { context.connListener.displayMessage("Failed to resume existing session"); return false; } @@ -364,7 +367,7 @@ public class NvConnection { private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context) throws IOException, XmlPullParserException { // Launch the app since it's not running - if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) { + if (!h.launchApp(context, "launch", context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) { context.connListener.displayMessage("Failed to launch application"); return false; } diff --git a/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java b/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java index 347f7402..ccb356fa 100644 --- a/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java +++ b/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java @@ -29,6 +29,7 @@ public class StreamConfiguration { private int encryptionFlags; private int colorRange; private int colorSpace; + private boolean persistGamepadsAfterDisconnect; public static class Builder { private StreamConfiguration config = new StreamConfiguration(); @@ -109,6 +110,11 @@ public class StreamConfiguration { return this; } + public StreamConfiguration.Builder setPersistGamepadsAfterDisconnect(boolean value) { + config.persistGamepadsAfterDisconnect = value; + return this; + } + public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) { config.clientRefreshRateX100 = refreshRateX100; return this; @@ -231,6 +237,10 @@ public class StreamConfiguration { return attachedGamepadMask; } + public boolean getPersistGamepadsAfterDisconnect() { + return persistGamepadsAfterDisconnect; + } + public int getClientRefreshRateX100() { return clientRefreshRateX100; } diff --git a/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java b/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java index 2b5fe44c..44ed5962 100644 --- a/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java +++ b/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java @@ -75,6 +75,7 @@ public class ComputerDetails { public PairingManager.PairState pairState; public int runningGameId; public String rawAppList; + public boolean nvidiaServer; public ComputerDetails() { // Use defaults @@ -143,6 +144,7 @@ public class ComputerDetails { this.httpsPort = details.httpsPort; this.pairState = details.pairState; this.runningGameId = details.runningGameId; + this.nvidiaServer = details.nvidiaServer; this.rawAppList = details.rawAppList; } diff --git a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java index 4c2719d8..184b3691 100644 --- a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java +++ b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java @@ -342,11 +342,10 @@ public class NvHTTP { return new ComputerDetails.AddressTuple(address, port); } - - public ComputerDetails getComputerDetails(boolean likelyOnline) throws IOException, XmlPullParserException { + + public ComputerDetails getComputerDetails(String serverInfo) throws IOException, XmlPullParserException { ComputerDetails details = new ComputerDetails(); - String serverInfo = getServerInfo(likelyOnline); - + details.name = getXmlString(serverInfo, "hostname", false); if (details.name == null || details.name.isEmpty()) { details.name = "UNKNOWN"; @@ -368,12 +367,19 @@ public class NvHTTP { details.pairState = getPairState(serverInfo); details.runningGameId = getCurrentGame(serverInfo); - + + // The MJOLNIR codename was used by GFE but never by any third-party server + details.nvidiaServer = getXmlString(serverInfo, "state", true).contains("MJOLNIR"); + // We could reach it so it's online details.state = ComputerDetails.State.ONLINE; - + return details; } + + public ComputerDetails getComputerDetails(boolean likelyOnline) throws IOException, XmlPullParserException { + return getComputerDetails(getServerInfo(likelyOnline)); + } // This hack is Android-specific but we do it on all platforms // because it doesn't really matter @@ -731,27 +737,30 @@ public class NvHTTP { return new String(hexChars); } - public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException { + public boolean launchApp(ConnectionContext context, String verb, int appId, boolean enableHdr) throws IOException, XmlPullParserException { // Using an FPS value over 60 causes SOPS to default to 720p60, // so force it to 0 to ensure the correct resolution is set. We // used to use 60 here but that locked the frame rate to 60 FPS // on GFE 3.20.3. - int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 0 : context.streamConfig.getLaunchRefreshRate(); + int fps = context.isNvidiaServerSoftware && context.streamConfig.getLaunchRefreshRate() > 60 ? + 0 : context.streamConfig.getLaunchRefreshRate(); - // Using an unsupported resolution (not 720p, 1080p, or 4K) causes - // GFE to force SOPS to 720p60. This is fine for < 720p resolutions like - // 360p or 480p, but it is not ideal for 1440p and other resolutions. - // When we detect an unsupported resolution, disable SOPS unless it's under 720p. - // FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list boolean enableSops = context.streamConfig.getSops(); - if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 && - context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 && - context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) { - LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight); - enableSops = false; + if (context.isNvidiaServerSoftware) { + // Using an unsupported resolution (not 720p, 1080p, or 4K) causes + // GFE to force SOPS to 720p60. This is fine for < 720p resolutions like + // 360p or 480p, but it is not ideal for 1440p and other resolutions. + // When we detect an unsupported resolution, disable SOPS unless it's under 720p. + // FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list + if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 && + context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 && + context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) { + LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight); + enableSops = false; + } } - String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), "launch", + String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), verb, "appid=" + appId + "&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps + "&additionalStates=1&sops=" + (enableSops ? 1 : 0) + @@ -760,24 +769,11 @@ public class NvHTTP { (!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") + "&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) + "&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo() + - (context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") + - (context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : "")); - if (!getXmlString(xmlStr, "gamesession", true).equals("0")) { - // sessionUrl0 will be missing for older GFE versions - context.rtspSessionUrl = getXmlString(xmlStr, "sessionUrl0", false); - return true; - } - else { - return false; - } - } - - public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException { - String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), "resume", - "rikey="+bytesToHex(context.riKey.getEncoded()) + - "&rikeyid="+context.riKeyId + - "&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo()); - if (!getXmlString(xmlStr, "resume", true).equals("0")) { + "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() + + "&gcmap=" + context.streamConfig.getAttachedGamepadMask() + + "&gcpersist="+(context.streamConfig.getPersistGamepadsAfterDisconnect() ? 1 : 0)); + if ((verb.equals("launch") && !getXmlString(xmlStr, "gamesession", true).equals("0") || + (verb.equals("resume") && !getXmlString(xmlStr, "resume", true).equals("0")))) { // sessionUrl0 will be missing for older GFE versions context.rtspSessionUrl = getXmlString(xmlStr, "sessionUrl0", false); return true;