mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-06-17 14:21:08 +00:00
Improve LAN/WAN detection for IPv6 and cellular connections
This commit is contained in:
@@ -30,7 +30,6 @@ import com.limelight.preferences.PreferenceConfiguration;
|
|||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.ui.StreamView;
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.NetHelper;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
import com.limelight.utils.ShortcutHelper;
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
@@ -451,11 +450,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean vpnActive = NetHelper.isActiveNetworkVpn(this);
|
|
||||||
if (vpnActive) {
|
|
||||||
LimeLog.info("Detected active network is a VPN");
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setLaunchRefreshRate(prefConfig.fps)
|
.setLaunchRefreshRate(prefConfig.fps)
|
||||||
@@ -464,10 +458,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.setBitrate(prefConfig.bitrate)
|
.setBitrate(prefConfig.bitrate)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize(vpnActive ? 1024 : 1392) // Lower MTU on VPN
|
.setMaxPacketSize(1392)
|
||||||
.setRemoteConfiguration(vpnActive ? // Use remote optimizations on VPN
|
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO) // NvConnection will perform LAN and VPN detection
|
||||||
StreamConfiguration.STREAM_CFG_REMOTE :
|
|
||||||
StreamConfiguration.STREAM_CFG_AUTO)
|
|
||||||
.setHevcBitratePercentageMultiplier(75)
|
.setHevcBitratePercentageMultiplier(75)
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setEnableHdr(willStreamHdr)
|
.setEnableHdr(willStreamHdr)
|
||||||
@@ -480,7 +472,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert, needsInputBatching);
|
conn = new NvConnection(getApplicationContext(), host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert, needsInputBatching);
|
||||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
||||||
keyboardTranslator = new KeyboardTranslator();
|
keyboardTranslator = new KeyboardTranslator();
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,8 @@ public class ConnectionContext {
|
|||||||
public int negotiatedWidth, negotiatedHeight;
|
public int negotiatedWidth, negotiatedHeight;
|
||||||
public boolean negotiatedHdr;
|
public boolean negotiatedHdr;
|
||||||
|
|
||||||
|
public int negotiatedRemoteStreaming;
|
||||||
|
public int negotiatedPacketSize;
|
||||||
|
|
||||||
public int videoCapabilities;
|
public int videoCapabilities;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
package com.limelight.nvstream;
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.IpPrefix;
|
||||||
|
import android.net.LinkProperties;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.RouteInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@@ -36,6 +49,7 @@ public class NvConnection {
|
|||||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||||
private final boolean isMonkey;
|
private final boolean isMonkey;
|
||||||
private final boolean batchMouseInput;
|
private final boolean batchMouseInput;
|
||||||
|
private final Context appContext;
|
||||||
|
|
||||||
private static final int MOUSE_BATCH_PERIOD_MS = 5;
|
private static final int MOUSE_BATCH_PERIOD_MS = 5;
|
||||||
private Timer mouseInputTimer;
|
private Timer mouseInputTimer;
|
||||||
@@ -43,8 +57,9 @@ public class NvConnection {
|
|||||||
private short relMouseX, relMouseY, relMouseWidth, relMouseHeight;
|
private short relMouseX, relMouseY, relMouseWidth, relMouseHeight;
|
||||||
private short absMouseX, absMouseY, absMouseWidth, absMouseHeight;
|
private short absMouseX, absMouseY, absMouseWidth, absMouseHeight;
|
||||||
|
|
||||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert, boolean batchMouseInput)
|
public NvConnection(Context appContext, String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert, boolean batchMouseInput)
|
||||||
{
|
{
|
||||||
|
this.appContext = appContext;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.cryptoProvider = cryptoProvider;
|
this.cryptoProvider = cryptoProvider;
|
||||||
this.uniqueId = uniqueId;
|
this.uniqueId = uniqueId;
|
||||||
@@ -117,6 +132,112 @@ public class NvConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private InetAddress resolveServerAddress() {
|
||||||
|
try {
|
||||||
|
InetAddress[] addrs = InetAddress.getAllByName(context.serverAddress);
|
||||||
|
for (InetAddress addr : addrs) {
|
||||||
|
try (Socket s = new Socket()) {
|
||||||
|
s.setSoLinger(true, 0);
|
||||||
|
s.connect(new InetSocketAddress(addr, 47989), 1000);
|
||||||
|
return addr;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int detectServerConnectionType() {
|
||||||
|
ConnectivityManager connMgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Network activeNetwork = connMgr.getActiveNetwork();
|
||||||
|
if (activeNetwork != null) {
|
||||||
|
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
|
||||||
|
if (netCaps != null) {
|
||||||
|
if (netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||||
|
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
|
||||||
|
// VPNs are treated as remote connections
|
||||||
|
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||||
|
}
|
||||||
|
else if (netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||||
|
// Cellular is always treated as remote to avoid any possible
|
||||||
|
// issues with 464XLAT or similar technologies.
|
||||||
|
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the server address is on-link
|
||||||
|
LinkProperties linkProperties = connMgr.getLinkProperties(activeNetwork);
|
||||||
|
if (linkProperties != null) {
|
||||||
|
InetAddress serverAddress = resolveServerAddress();
|
||||||
|
|
||||||
|
// If the address is in the NAT64 prefix, always treat it as remote
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
IpPrefix nat64Prefix = linkProperties.getNat64Prefix();
|
||||||
|
if (nat64Prefix != null && nat64Prefix.contains(serverAddress)) {
|
||||||
|
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (RouteInfo route : linkProperties.getRoutes()) {
|
||||||
|
// Skip non-unicast routes (which are all we get prior to Android 13)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && route.getType() != RouteInfo.RTN_UNICAST) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first route that matches this address
|
||||||
|
if (route.matches(serverAddress)) {
|
||||||
|
// If there's no gateway, this is an on-link destination
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
// We want to use hasGateway() because getGateway() doesn't adhere
|
||||||
|
// to documented behavior of returning null for on-link addresses.
|
||||||
|
if (!route.hasGateway()) {
|
||||||
|
return StreamConfiguration.STREAM_CFG_LOCAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// getGateway() is documented to return null for on-link destinations,
|
||||||
|
// but it actually returns the unspecified address (0.0.0.0 or ::).
|
||||||
|
InetAddress gateway = route.getGateway();
|
||||||
|
if (gateway == null || gateway.isAnyLocalAddress()) {
|
||||||
|
return StreamConfiguration.STREAM_CFG_LOCAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We _should_ stop after the first matching route, but for some reason
|
||||||
|
// Android doesn't always report IPv6 routes in descending order of
|
||||||
|
// specificity and metric. To handle that case, we enumerate all matching
|
||||||
|
// routes, assuming that an on-link route will always be preferred.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||||
|
if (activeNetworkInfo != null) {
|
||||||
|
switch (activeNetworkInfo.getType()) {
|
||||||
|
case ConnectivityManager.TYPE_VPN:
|
||||||
|
case ConnectivityManager.TYPE_MOBILE:
|
||||||
|
case ConnectivityManager.TYPE_MOBILE_DUN:
|
||||||
|
case ConnectivityManager.TYPE_MOBILE_HIPRI:
|
||||||
|
case ConnectivityManager.TYPE_MOBILE_MMS:
|
||||||
|
case ConnectivityManager.TYPE_MOBILE_SUPL:
|
||||||
|
case ConnectivityManager.TYPE_WIMAX:
|
||||||
|
// VPNs and cellular connections are always remote connections
|
||||||
|
return StreamConfiguration.STREAM_CFG_REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't determine the connection type, let moonlight-common-c decide.
|
||||||
|
return StreamConfiguration.STREAM_CFG_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean startApp() throws XmlPullParserException, IOException
|
private boolean startApp() throws XmlPullParserException, IOException
|
||||||
{
|
{
|
||||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||||
@@ -172,6 +293,18 @@ public class NvConnection {
|
|||||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We will perform some connection type detection if the caller asked for it
|
||||||
|
if (context.streamConfig.getRemote() == StreamConfiguration.STREAM_CFG_AUTO) {
|
||||||
|
context.negotiatedRemoteStreaming = detectServerConnectionType();
|
||||||
|
context.negotiatedPacketSize =
|
||||||
|
context.negotiatedRemoteStreaming == StreamConfiguration.STREAM_CFG_REMOTE ?
|
||||||
|
1024 : context.streamConfig.getMaxPacketSize();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
context.negotiatedRemoteStreaming = context.streamConfig.getRemote();
|
||||||
|
context.negotiatedPacketSize = context.streamConfig.getMaxPacketSize();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Video stream format will be decided during the RTSP handshake
|
// Video stream format will be decided during the RTSP handshake
|
||||||
//
|
//
|
||||||
@@ -311,8 +444,8 @@ public class NvConnection {
|
|||||||
context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl,
|
context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl,
|
||||||
context.negotiatedWidth, context.negotiatedHeight,
|
context.negotiatedWidth, context.negotiatedHeight,
|
||||||
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||||
context.streamConfig.getMaxPacketSize(),
|
context.negotiatedPacketSize, context.negotiatedRemoteStreaming,
|
||||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
|
context.streamConfig.getAudioConfiguration().toInt(),
|
||||||
context.streamConfig.getHevcSupported(),
|
context.streamConfig.getHevcSupported(),
|
||||||
context.negotiatedHdr,
|
context.negotiatedHdr,
|
||||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||||
|
|||||||
Reference in New Issue
Block a user