Modernize HTTPS launch/resume for Sunshine

This commit is contained in:
Cameron Gutman
2023-02-20 22:04:41 -06:00
parent b961636f02
commit 80acd9b9eb
6 changed files with 52 additions and 39 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;