Compare commits

..

25 Commits

Author SHA1 Message Date
Cameron Gutman cc7ac79fa6 Version 9.6.4 2020-07-10 18:31:41 -07:00
Cameron Gutman 4c5c27dfc1 Re-enable the max operating rate trick on Android 10 except on the Mi 10 Lite 5G
It still provides nice performance gains on Pixel 2 running Android 10
2020-07-10 18:29:29 -07:00
Cameron Gutman 4aabfbd52e Add missing jlong cast to fix Lint warning 2020-07-07 01:10:10 -05:00
Cameron Gutman 6eab842361 Fix Lint error due to extra translated strings 2020-07-07 01:05:10 -05:00
a6969 b729dfd702 Added Ukrainian language (#857)
* Added Ukrainian language strings.xml

Translated the application into Ukrainian language.
2020-07-07 00:59:42 -05:00
Cameron Gutman 6366840781 Update common-c to remove FEC validation assert that fails on GFE 3.20.4 2020-07-07 00:58:44 -05:00
Cameron Gutman 704a2ee90b Propagate exceptions caused by GFE response parsing errors 2020-07-07 00:57:37 -05:00
Cameron Gutman 484be9bfe6 Wrap and propagate unexpected exceptions 2020-07-07 00:52:11 -05:00
Cameron Gutman a99e070c26 Fix missing return causing invalid parameters to be passed to LiStartConnection() 2020-07-07 00:47:12 -05:00
Cameron Gutman bf803f88af Refactor TLS initialization code 2020-07-06 02:32:06 -05:00
Cameron Gutman 9af6febca5 Fix pairing issue due to picking up a final local variable instead of a class member 2020-07-06 02:30:49 -05:00
Cameron Gutman 0101d0a1bd Fix TLS error when connecting to GFE 3.20.4 on Android 4.x 2020-07-06 01:44:35 -05:00
Cameron Gutman 266874609d Fix hostname validation for CA-issued certificates 2020-07-04 20:09:06 -05:00
Cameron Gutman 2ba7feedfc Fix several Lint warnings 2020-07-04 15:41:41 -05:00
Cameron Gutman 43c67b4939 Avoid using max operating rate on Android Q and non-Qualcomm devices 2020-07-01 11:26:40 -05:00
Cameron Gutman 2d9915e43a Enable GWP-ASan on Android 11 2020-07-01 11:07:53 -05:00
Cameron Gutman 2329b41bce Rethrow the original validation error if the cert isn't pinned or self-signed 2020-06-29 11:29:33 -07:00
Cameron Gutman 536496184e Use the default X509TrustManager to validate non-pinned certificates
This allows the certificate to be rotated without re-adding the PC.
2020-06-29 11:20:14 -07:00
Cameron Gutman 429c32477c Version 9.6.1 2020-06-25 22:15:24 -07:00
Cameron Gutman f5d51b2061 Disable PiP option on Fire OS due to Amazon guidelines 2020-06-24 17:26:58 -07:00
Zero O 2ad1aaa277 Update strings.xml (#850)
update translation
2020-06-24 17:21:54 -07:00
Zero O 3afd32dbc1 Update strings.xml (#851)
update translation
2020-06-24 17:21:45 -07:00
Cameron Gutman 092830ed07 Remove button emulation
It was never well documented to users and it really only makes sense
with much older controllers that don't have Start or Select buttons.
2020-06-23 22:00:56 -07:00
Cameron Gutman d118a6d3ff Prevent edges of analog sticks from being clipped 2020-06-23 21:48:50 -07:00
Cameron Gutman fe97ffdc2f Slightly reduce size of analog sticks to allow a gap before the edge of the screen
This reduces false analog stick releases caused when the finger goes off the display's touch area.
2020-06-23 21:36:33 -07:00
26 changed files with 452 additions and 251 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
minSdkVersion 16
targetSdkVersion 30
versionName "9.6"
versionCode = 231
versionName "9.6.4"
versionCode = 237
}
flavorDimensions "root"
+1
View File
@@ -42,6 +42,7 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:installLocation="auto"
android:gwpAsanMode="always"
android:theme="@style/AppTheme">
<provider
android:name=".PosterContentProvider"
+1 -1
View File
@@ -590,7 +590,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
listView.requestFocus();
}
public class AppObject {
public static class AppObject {
public final NvApp app;
public boolean isRunning;
+1 -1
View File
@@ -719,7 +719,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
registerForContextMenu(listView);
}
public class ComputerObject {
public static class ComputerObject {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {
@@ -102,9 +102,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
@@ -124,10 +122,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Date now = new Date();
@@ -152,8 +148,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
throw new RuntimeException(e);
}
@@ -40,12 +40,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1;
private static final int EMULATING_SELECT = 0x2;
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private final Vector2d inputVector = new Vector2d();
private final SparseArray<InputDeviceContext> inputDeviceContexts = new SparseArray<>();
@@ -1422,41 +1416,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return false;
}
// Check if we're emulating the select button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
{
context.inputMap &= ~ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
} catch (InterruptedException ignored) {}
}
}
// Check if we're emulating the special button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
{
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
} catch (InterruptedException ignored) {}
}
}
sendControllerInputPacket(context);
if (context.pendingExit && context.inputMap == 0) {
@@ -1576,29 +1535,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.pendingExit = true;
}
// Start+LB acts like select for controllers with one button
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
context.inputMap |= ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
// We detect select+start or start+RB as the special button combo
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// We don't need to send repeat key down events, but the platform
// sends us events that claim to be repeats but they're from different
// devices, so we just send them all and deal with some duplicates.
@@ -1680,7 +1616,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
usbDeviceContexts.put(controller.getControllerId(), context);
}
class GenericControllerContext {
static class GenericControllerContext {
public int id;
public boolean external;
@@ -1744,8 +1680,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean hasJoystickAxes;
public boolean pendingExit;
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 100);
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
@@ -22,7 +22,7 @@ import java.util.Timer;
import java.util.TimerTask;
public class VirtualController {
public class ControllerInputContext {
public static class ControllerInputContext {
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
@@ -167,11 +167,11 @@ public class VirtualControllerConfigurationLoader {
private static final int DPAD_BASE_Y = 41;
private static final int DPAD_SIZE = 30;
private static final int ANALOG_L_BASE_X = 4;
private static final int ANALOG_L_BASE_Y = 1;
private static final int ANALOG_R_BASE_X = 96;
private static final int ANALOG_L_BASE_X = 6;
private static final int ANALOG_L_BASE_Y = 4;
private static final int ANALOG_R_BASE_X = 98;
private static final int ANALOG_R_BASE_Y = 42;
private static final int ANALOG_SIZE = 28;
private static final int ANALOG_SIZE = 26;
private static final int L3_R3_BASE_Y = 60;
@@ -334,11 +334,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
}
// Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy
// our (ludicrous) operating rate requirement.
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
if (MediaCodecHelper.decoderSupportsMaxOperatingRate(selectedDecoderName)) {
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
}
}
configuredFormat = videoFormat;
@@ -354,6 +354,21 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderSupportsMaxOperatingRate(String decoderName) {
// Operate at maximum rate to lower latency as much as possible on
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
// but that will actually result in the decoder crashing if it can't satisfy
// our (ludicrous) operating rate requirement. This seems to cause reliable
// crashes on the Xiaomi Mi 10 lite 5G on Android 10, so we'll disable it
// on that device and all non-Qualcomm devices to be safe.
//
// NB: Even on Android 10, this optimization still provides significant
// performance gains on Pixel 2.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
isDecoderInList(qualcommDecoderPrefixes, decoderName) &&
!Build.DEVICE.equalsIgnoreCase("monet");
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo, String mimeType) {
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -362,7 +362,7 @@ public class CachedAppAssetLoader {
return false;
}
public class LoaderTuple {
public static class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
@@ -43,25 +43,26 @@ public class NvConnection {
this.context = new ConnectionContext();
this.context.streamConfig = config;
this.context.serverCert = serverCert;
try {
// This is unique per connection
this.context.riKey = generateRiAesKey();
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
}
this.context.riKeyId = generateRiKeyId();
// This is unique per connection
this.context.riKey = generateRiAesKey();
context.riKeyId = generateRiKeyId();
this.isMonkey = ActivityManager.isUserAMonkey();
}
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits
keyGen.init(128);
return keyGen.generateKey();
private static SecretKey generateRiAesKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits
keyGen.init(128);
return keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static int generateRiKeyId() {
@@ -238,6 +239,7 @@ public class NvConnection {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, e.getErrorCode());
return;
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
@@ -1,20 +1,26 @@
package com.limelight.nvstream.http;
import android.os.Build;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
@@ -24,11 +30,16 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
@@ -42,7 +53,6 @@ import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.http.PairingManager.PairState;
import okhttp3.ConnectionPool;
import okhttp3.Handshake;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@@ -66,34 +76,36 @@ public class NvHTTP {
private OkHttpClient httpClient;
private OkHttpClient httpClientWithReadTimeout;
private X509TrustManager defaultTrustManager;
private X509TrustManager trustManager;
private X509KeyManager keyManager;
private X509Certificate serverCert;
void setServerCert(X509Certificate serverCert) {
this.serverCert = serverCert;
trustManager = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
// Check the server certificate if we've paired to this host
if (!certs[0].equals(NvHTTP.this.serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
};
}
private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
// Set up TrustManager
setServerCert(serverCert);
private static X509TrustManager getDefaultTrustManager() {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
return (X509TrustManager) tm;
}
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
throw new IllegalStateException("No X509 trust manager found");
}
private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) {
keyManager = new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
@@ -109,9 +121,51 @@ public class NvHTTP {
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
};
// Ignore differences between given hostname and certificate hostname
defaultTrustManager = getDefaultTrustManager();
trustManager = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
try {
// Try the default trust manager first to allow pairing with certificates
// that chain up to a trusted root CA. This will raise CertificateException
// if the certificate is not trusted (expected for GFE's self-signed certs).
defaultTrustManager.checkServerTrusted(certs, authType);
} catch (CertificateException e) {
// Check the server certificate if we've paired to this host
if (certs.length == 1 && NvHTTP.this.serverCert != null) {
if (!certs[0].equals(NvHTTP.this.serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
else {
// The cert chain doesn't look like a self-signed cert or we don't have
// a certificate pinned, so re-throw the original validation error.
throw e;
}
}
}
};
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
public boolean verify(String hostname, SSLSession session) {
try {
Certificate[] certificates = session.getPeerCertificates();
if (certificates.length == 1 && certificates[0].equals(NvHTTP.this.serverCert)) {
// Allow any hostname if it's our pinned cert
return true;
}
} catch (SSLPeerUnverifiedException e) {
e.printStackTrace();
}
// Fall back to default HostnameVerifier for validating CA-issued certs
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
}
};
httpClient = new OkHttpClient.Builder()
@@ -131,7 +185,9 @@ public class NvHTTP {
// started by other Moonlight clients.
this.uniqueId = "0123456789ABCDEF";
initializeHttpState(serverCert, cryptoProvider);
this.serverCert = serverCert;
initializeHttpState(cryptoProvider);
try {
// The URI constructor takes care of escaping IPv6 literals
@@ -270,11 +326,7 @@ public class NvHTTP {
// This has some extra logic to always report unpaired if the pinned cert isn't there
details.pairState = getPairState(serverInfo);
try {
details.runningGameId = getCurrentGame(serverInfo);
} catch (NumberFormatException e) {
details.runningGameId = 0;
}
details.runningGameId = getCurrentGame(serverInfo);
// We could reach it so it's online
details.state = ComputerDetails.State.ONLINE;
@@ -290,24 +342,21 @@ public class NvHTTP {
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build();
// TLS 1.2 is not enabled by default prior to Android 5.0, so we'll need a custom
// SSLSocketFactory in order to connect to GFE 3.20.4 which requires TLSv1.2 or later.
// We don't just always use TLSv12SocketFactory because explicitly specifying TLS versions
// prevents later TLS versions from being negotiated even if client and server otherwise
// support them.
return client.newBuilder().sslSocketFactory(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
sc.getSocketFactory() : new TLSv12SocketFactory(sc),
trustManager).build();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
public X509Certificate getCertificateIfTrusted() {
try {
Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute();
Handshake handshake = resp.handshake();
if (handshake != null) {
return (X509Certificate)handshake.peerCertificates().get(0);
}
} catch (IOException ignored) {}
return null;
}
// Read timeout should be enabled for any HTTP query that requires no outside action
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
@@ -316,10 +365,6 @@ public class NvHTTP {
Request request = new Request.Builder().url(url).get().build();
Response response;
if (serverCert == null && !url.startsWith(baseUrlHttp)) {
throw new IllegalStateException("Attempted HTTPS fetch without pinned cert");
}
if (enableReadTimeout) {
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
}
@@ -379,11 +424,6 @@ public class NvHTTP {
}
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
// If we don't have a server cert, we can't be paired even if the host thinks we are
if (serverCert == null) {
return PairState.NOT_PAIRED;
}
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
return PairState.NOT_PAIRED;
}
@@ -394,11 +434,7 @@ public class NvHTTP {
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
return 0;
}
return Long.parseLong(str);
} else {
return 0;
}
@@ -407,11 +443,7 @@ public class NvHTTP {
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
return 0;
}
return Long.parseLong(str);
} else {
return 0;
}
@@ -428,11 +460,7 @@ public class NvHTTP {
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "ServerCodecModeSupport");
if (str != null) {
try {
return Long.parseLong(str);
} catch (NumberFormatException e) {
return 0;
}
return Long.parseLong(str);
} else {
return 0;
}
@@ -588,36 +616,23 @@ public class NvHTTP {
}
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
int[] appVersionQuad = getServerAppVersionQuad(serverInfo);
if (appVersionQuad != null) {
return appVersionQuad[0];
}
else {
return 0;
}
return getServerAppVersionQuad(serverInfo)[0];
}
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
try {
String serverVersion = getServerVersion(serverInfo);
if (serverVersion == null) {
LimeLog.warning("Missing server version field");
return null;
}
String[] serverVersionSplit = serverVersion.split("\\.");
if (serverVersionSplit.length != 4) {
LimeLog.warning("Malformed server version field");
return null;
}
int[] ret = new int[serverVersionSplit.length];
for (int i = 0; i < ret.length; i++) {
ret[i] = Integer.parseInt(serverVersionSplit[i]);
}
return ret;
} catch (NumberFormatException e) {
e.printStackTrace();
return null;
String serverVersion = getServerVersion(serverInfo);
if (serverVersion == null) {
throw new IllegalArgumentException("Missing server version field");
}
String[] serverVersionSplit = serverVersion.split("\\.");
if (serverVersionSplit.length != 4) {
throw new IllegalArgumentException("Malformed server version field: "+serverVersion);
}
int[] ret = new int[serverVersionSplit.length];
for (int i = 0; i < ret.length; i++) {
ret[i] = Integer.parseInt(serverVersionSplit[i]);
}
return ret;
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
@@ -695,4 +710,62 @@ public class NvHTTP {
return true;
}
// Based on example code from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
private static class TLSv12SocketFactory extends SSLSocketFactory {
private SSLSocketFactory internalSSLSocketFactory;
public TLSv12SocketFactory(SSLContext context) {
internalSSLSocketFactory = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket() throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket());
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private Socket enableTLSv12OnSocket(Socket socket) {
if (socket instanceof SSLSocket) {
// TLS 1.2 is not enabled by default prior to Android 5.0. We must enable it
// explicitly to ensure we can communicate with GFE 3.20.4 which blocks TLS 1.0.
((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
}
return socket;
}
}
}
@@ -55,6 +55,10 @@ public class PairingManager {
private static byte[] hexToBytes(String s) {
int len = s.length();
if (len % 2 != 0) {
throw new IllegalArgumentException("Illegal string length: "+len);
}
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
@@ -74,7 +78,7 @@ public class PairingManager {
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
} catch (CertificateException e) {
e.printStackTrace();
return null;
throw new RuntimeException(e);
}
}
else {
@@ -314,9 +318,8 @@ public class PairingManager {
return md.digest(data);
}
catch (NoSuchAlgorithmException e) {
// Shouldn't ever happen
e.printStackTrace();
return null;
throw new RuntimeException(e);
}
}
}
@@ -332,9 +335,8 @@ public class PairingManager {
return md.digest(data);
}
catch (NoSuchAlgorithmException e) {
// Shouldn't ever happen
e.printStackTrace();
return null;
throw new RuntimeException(e);
}
}
}
@@ -104,12 +104,6 @@ public class AddComputerManually extends Activity {
try {
ComputerDetails details = new ComputerDetails();
details.manualAddress = host;
try {
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
details.serverCert = http.getCertificateIfTrusted();
} catch (IOException ignored) {}
success = managerBinder.addComputerBlocking(details);
} catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
@@ -154,9 +154,11 @@ public class StreamSettings extends Activity {
}
}
// Remove PiP mode on devices pre-Oreo or where the feature is not available (some low RAM devices)
// Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices),
// and on Fire OS where it violates the Amazon App Store guidelines for some reason.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture")) {
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") ||
getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_ui_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
+2 -2
View File
@@ -157,7 +157,7 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
decodeUnit->frameNumber, (jlong)decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
@@ -178,7 +178,7 @@ int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
decodeUnit->frameNumber,
decodeUnit->receiveTimeMs);
(jlong)decodeUnit->receiveTimeMs);
if ((*env)->ExceptionCheck(env)) {
// We will crash here
(*JVM)->DetachCurrentThread(JVM);
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<item>Автоматичний вибір декодера</item>
<item>Примусове програмне декодування</item>
<item>Примусове апаратне декодування</item>
</string-array>
<string-array name="video_format_names">
<item>Використовувати H.265 тільки якщо безпечно</item>
<item>Завжди використовувати H.265 якщо доступно</item>
<item>Ніколи не використовувати H.265</item>
</string-array>
</resources>
+195
View File
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Подивитися список ігор</string>
<string name="pcview_menu_pair_pc">Створити пару з ПК</string>
<string name="pcview_menu_unpair_pc">Розірвати пару</string>
<string name="pcview_menu_send_wol">Надіслати Wake-On-LAN запит</string>
<string name="pcview_menu_delete_pc">Видалити ПК</string>
<!-- Pair messages -->
<string name="pairing">Створення пари…</string>
<string name="pair_pc_offline">Комп\'ютер вимкнений або знаходиться не в мережі</string>
<string name="pair_pc_ingame">Комп\'ютер в даний момент знаходиться в грі. Ви повинні закрити гру перед створенням пари.</string>
<string name="pair_pairing_title">Створення пари</string>
<string name="pair_pairing_msg">Будь ласка, введіть цей PIN на ПК:</string>
<string name="pair_incorrect_pin">Неправильний PIN</string>
<string name="pair_fail">Створення пари не вдалося</string>
<!-- WOL messages -->
<string name="wol_pc_online">Комп\'ютер в мережі</string>
<string name="wol_no_mac">Неможливо розбудити ПК бо GFE не відправили MAC адреса</string>
<string name="wol_waking_pc">Пробудження ПК…</string>
<string name="wol_waking_msg">Пробудження ПК може зайняти кілька секунд.
Якщо цього не відбувається, упевніться що Wake-On-LAN налаштований правильно.
</string>
<string name="wol_fail">Помилка при відправці Wake-On-LAN пакетів</string>
<!-- Unpair messages -->
<string name="unpairing">Розрив пари…</string>
<string name="unpair_success">Розрив пари закінчився успішно</string>
<string name="unpair_fail">Розрив пари не вдався</string>
<string name="unpair_error">Пристрій не було спарено</string>
<!-- Errors -->
<string name="error_pc_offline">Комп\'ютер вимкнений або знаходиться не в мережі</string>
<string name="error_manager_not_running">Сервіс ComputerManager не запущено. Будь ласка, зачекайте кілька секунд або запустіть програму.</string>
<string name="error_unknown_host">Неможливо знайти хост</string>
<string name="error_404">GFE повернув помилку HTTP 404. Переконайтеся що Ваш ПК використовує підтримуваний GPU.
Використання програм для віддаленого доступу також може викликати цю помилку. Спробуйте перезавантажити комп\'ютер або перевстановити GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Створення з\'єднання</string>
<string name="conn_establishing_msg">Підключення</string>
<string name="conn_metered">Увага: Відбувається вимір Вашого мережевого з\'єднання!</string>
<string name="conn_client_latency">Середня затримка декодування кадру:</string>
<string name="conn_client_latency_hw">затримка апаратного декодування:</string>
<string name="conn_hardware_latency">Середня затримка апаратного декодування:</string>
<string name="conn_starting">Запуск</string>
<string name="conn_error_title">Помилка з\'єднання</string>
<string name="conn_error_msg">Запуск не вдався</string>
<string name="conn_terminated_title">З\'єднання припинено</string>
<string name="conn_terminated_msg">Підключення було перервано</string>
<!-- General strings -->
<string name="ip_hint">IP-адреса комп\'ютера з GeForce</string>
<string name="searching_pc">Пошук комп\'ютерів із запущеним GameStream…\n\n
Переконайтеся що GameStream увімкнений в налаштуваннях GeForce Experience в розділі SHIELD.</string>
<string name="yes">Так</string>
<string name="no">Ні</string>
<string name="lost_connection">З\'єднання втрачено з ПК</string>
<!-- AppList activity -->
<string name="applist_menu_resume">Відновити сесію</string>
<string name="applist_menu_quit">Вийти з сесії</string>
<string name="applist_menu_quit_and_start">Вийти з поточної гри і запустити</string>
<string name="applist_menu_cancel">Скасування</string>
<string name="applist_menu_tv_channel">Додати на канал</string>
<string name="applist_refresh_title">Список додатків</string>
<string name="applist_refresh_msg">Оновлення додатків…</string>
<string name="applist_refresh_error_title">Помилка</string>
<string name="applist_refresh_error_msg">Помилка при отриманні списку додатків</string>
<string name="applist_quit_app">Вихід з</string>
<string name="applist_quit_success">Вихід відбувся успішно з</string>
<string name="applist_quit_fail">Помилка при виході</string>
<string name="applist_quit_confirmation">Ви впевнені, що хочете вийти з запущеного додатку? Усі незбережені дані будуть втрачені.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Додавання ПК вручну</string>
<string name="msg_add_pc">З\'єднання з ПК…</string>
<string name="addpc_fail">Неможливо підключитися до вибраного комп\'ютера. Переконайтеся, що необхідні порти дозволені в налаштуваннях брандмауера.</string>
<string name="addpc_success">Комп\'ютер доданий успішно</string>
<string name="addpc_unknown_host">Неможливо знайти ПК за вказаною адресою. Переконайтеся, що Ви не зробили помилок під час його написання.</string>
<string name="addpc_enter_ip">Ви повинні ввести IP адресу</string>
<!-- Preferences -->
<string name="category_basic_settings">Загальні Налаштування</string>
<string name="title_resolution_list">Виберіть розширення і частоту кадрів</string>
<string name="summary_resolution_list">Вибір занадто високого значеня для свого пристрою може викликати гальма або вильоти</string>
<string name="title_seekbar_bitrate">Виберіть бітрейт відео</string>
<string name="summary_seekbar_bitrate">Низький бітрейт зменшить зависання. Збільшення бітрейта поліпшить якість зображення.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Розтягувати відео на весь екран</string>
<string name="title_checkbox_disable_warnings">Відключити повідомлення з попередженнями</string>
<string name="summary_checkbox_disable_warnings">Вимкнути екранні попередження про з\'єднання під час трансляції</string>
<string name="category_audio_settings">Аудіо Налаштування</string>
<string name="title_audio_config_list">Конфігурація об\'ємного звуку</string>
<string name="summary_audio_config_list">Увімкнути 5.1 або 7.1 об\'ємний звук для систем домашнього кінотеатру</string>
<string name="title_checkbox_multi_controller">Підтримка декількох контролерів</string>
<string name="summary_checkbox_multi_controller">Коли вимкнено, всі контролери визначаються як один</string>
<string name="title_seekbar_deadzone">Регулювати мертву зону аналогового стіку</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Драйвер контролерів Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Увімкнути вбудований USB драйвер для пристроїв без власної підтримки контролерів Xbox</string>
<string name="category_ui_settings">Налаштування інтерфейсу</string>
<string name="title_language_list">Мова</string>
<string name="summary_language_list">Мова, яка буде використовуватися в Moonlight</string>
<string name="title_checkbox_small_icon_mode">Використовувати маленькі іконки</string>
<string name="summary_checkbox_small_icon_mode">Використовувати маленькі іконки в сітці для відображення більшої кількості елементів на екрані</string>
<string name="category_host_settings">Налаштування Хоста</string>
<string name="title_checkbox_enable_sops">Оптимізувати ігрові налаштування</string>
<string name="summary_checkbox_enable_sops">Дозволити GFE змінювати налаштування ігор для оптимальної трансляції</string>
<string name="title_checkbox_host_audio">Програвати звук на ПК</string>
<string name="summary_checkbox_host_audio">Програвати звук на комп\'ютері і поточному пристрої</string>
<string name="category_advanced_settings">Розширені Налаштування</string>
<string name="title_video_format">Змінити налаштування H.265</string>
<string name="summary_video_format">H.265 знижує вимоги до пропускної здатності, але вимагає дуже нового пристрою</string>
<string name="category_on_screen_controls_settings">Налаштування дисплею з кнопками</string>
<string name="title_checkbox_show_onscreen_controls">Показувати екранні кнопки</string>
<string name="summary_checkbox_show_onscreen_controls">Відображати оверлей віртуального контролера на сенсорному екрані</string>
<string name="title_only_l3r3">Показувати тільки L3 і R3</string>
<string name="summary_only_l3r3">Приховувати всі екранні кнопки крім L3 і R3</string>
<string name="scut_deleted_pc">ПК видалений</string>
<string name="scut_not_paired">ПК не сполучений</string>
<string name="help_loading_title">Перегляд Допомоги</string>
<string name="help_loading_msg">Завантаження сторінки допомоги…</string>
<string name="pair_already_in_progress">Сполучення вже в процесі</string>
<string name="help">Допомога</string>
<string name="applist_connect_msg">Підключення до ПК…</string>
<string name="title_decoding_error">Збій відео декодера</string>
<string name="message_decoding_error">Стався збій Moonlight через проблеми з відео декодером даного пристрою. Спробуйте змінити налаштування трансляції якщо збої будуть продовжуватися.</string>
<string name="title_decoding_reset">Відео Налаштування Скинуті</string>
<string name="message_decoding_reset">Відео декодер Вашого пристрою давав збої з вибраними налаштуваннями. Налаштування трансляції були скинуті до значень за замовчуванням.</string>
<string name="error_usb_prohibited">USB доступ заборонений адміністратором пристрою. Перевірте налаштування Knox або MDM.</string>
<string name="addpc_wrong_sitelocal">Адреса вказана невірно. Ви повинні ввести публічну IP-адресу Вашого роутера для передачі через інтернет.</string>
<string name="title_checkbox_enable_pip">Увімкнути перегляд у режимі \"Картинка в картинці\"</string>
<string name="summary_checkbox_enable_pip">Дозволяє переглядати трансляцію (але не керувати нею) під час роботи в інших додатках</string>
<string name="title_checkbox_usb_bind_all">Відхилити підтримку контролерів Android</string>
<string name="summary_checkbox_usb_bind_all">Змушує USB драйвер Moonlight взяти на себе роботу з усіма підтримуваними Xbox геймпадами</string>
<string name="title_checkbox_mouse_emulation">Емуляція миші на геймпаді</string>
<string name="summary_checkbox_mouse_emulation">Довге натиснення кнопки Start перемкне геймпад в режим миші</string>
<string name="title_reset_osc">Скинути схему розташування екранних кнопок</string>
<string name="summary_reset_osc">Повертає всі екранні елементи керування до їх розташуванням за замовчуванням</string>
<string name="dialog_title_reset_osc">Скинути Схему</string>
<string name="dialog_text_reset_osc">Ви впевнені що хочете видалити збережену схему розташування кнопок?</string>
<string name="toast_reset_osc_success">Екранні елементи керування повернуті до положень за замовчуванням</string>
<string name="title_disable_frame_drop">Ніколи не пропускати кадри</string>
<string name="summary_disable_frame_drop">Може зменшити мікрозависання на деяких пристроях, але також збільшити затримку</string>
<string name="title_enable_hdr">Увімкнути HDR (Експериментально)</string>
<string name="summary_enable_hdr">Транслювати в HDR якщо гра і GPU комп\'ютера підтримують це. HDR вимагає відеокарти GTX 1000 серії або більш нової.</string>
<string name="title_checkbox_vibrate_osc">Увімкнути вібрацію</string>
<string name="title_fps_list">Частота кадрів</string>
<string name="applist_menu_details">Деталі</string>
<string name="applist_menu_scut">Створити ярлик</string>
<string name="category_input_settings">Налаштування введення</string>
<string name="title_checkbox_touchscreen_trackpad">Використовувати сенсорний екран як трекпад</string>
<string name="summary_checkbox_touchscreen_trackpad">Якщо увімкнено, сенсорний екран працює як трекпад. Якщо його вимкнено, сенсорний екран безпосередньо керує курсором миші.</string>
<string name="delete_pc_msg">Ви впевнені що хочете видалити цей ПК?</string>
<string name="pcview_menu_details">Деталі</string>
<string name="poor_connection_msg">Слабке з\'єднання з ПК</string>
<string name="title_details">Деталі</string>
<string name="title_enable_perf_overlay">Увімкнути відображення статистики</string>
<string name="title_unlock_fps">Розблокувати всі можливі частоти оновлення</string>
<string name="applist_details_id">ID додатку:</string>
<string name="title_checkbox_vibrate_fallback">Емуляція вібровіддачі</string>
<string name="summary_checkbox_vibrate_osc">Вібрація пристрою для емуляції вібровіддачі при екранному управлінні</string>
<string name="summary_checkbox_vibrate_fallback">Вібрувати пристрій для емуляції вібровіддачі для геймпадов без підтримки вібрації</string>
<string name="summary_checkbox_mouse_nav_buttons">Включення цієї опції може привести до неправильної роботи правої кнопки миші на деяких пристроях</string>
<string name="title_checkbox_flip_face_buttons">Відкідні кнопки</string>
<string name="summary_checkbox_flip_face_buttons">Перемикає лицьові кнопки A/B і X/Y для геймпадів та екранних елементів керування</string>
<string name="scut_pc_not_found">ПК не знайдено</string>
<string name="unable_to_pin_shortcut">Поточний лаунчер не дозволяє створювати закріплені ярлики</string>
<string name="title_checkbox_mouse_nav_buttons">Увімкнути кнопки вперед і назад для миші</string>
<string name="slow_connection_msg">Повільне підключення до ПК\nЗменшити бітрейт</string>
<string name="summary_unlock_fps">Трансляція зі швидкістю 90 або 120 кадрів в секунду може зменшити затримку на пристроях високого класу, але може викликати затримки або збій на пристроях без підтримки цього функціоналу</string>
<string name="summary_enable_perf_overlay">Відображення накладення на екрані з інформацією про продуктивність під час трансляції в режимі реального часу</string>
<string name="perf_overlay_text">Розширення відео: %1$s\nДекодер: %2$s\nРозрахункова частота кадрів ПК-хоста: %3$.2f FPS\nВхідна частота кадрів з мережі: %4$.2f FPS\nЧастота кадрів під час рендерінгу: %5$.2f FPS\nВідкинутих кадрів вашою мережею: %6$.2f%%\nСередній час отримання: %7$.2f ms\nСередній час декодування: %8$.2f ms</string>
<string name="summary_fps_list">Збільшення для більш плавного відео потоку. Зменшіть для кращої продуктивності на більш слабких пристроях.</string>
<string name="scut_invalid_uuid">Зазначений ПК недійсний</string>
<string name="scut_invalid_app_id">Зазначений додаток недійсне</string>
<string name="title_osc_opacity">Змінити непрозорість екранних елементів керування</string>
<string name="summary_osc_opacity">Зробити екранні елементи керування більш/менш прозорими</string>
<string name="dialog_title_osc_opacity">Зміна непрозорості</string>
<string name="suffix_osc_opacity">%</string>
<string name="title_enable_post_stream_toast">Показувати затримку після трансляції</string>
<string name="summary_enable_post_stream_toast">Вивести інформаційне повідомлення про затримку після закінчення потоку</string>
</resources>
+2 -18
View File
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight Root</string>
<!-- Shortcut strings -->
<string name="scut_deleted_pc"> 电脑已删除 </string>
@@ -189,24 +187,10 @@
<string name="summary_video_format">H.265能降低视频带宽需求,但需要较新的设备才能支持</string>
<string name="title_enable_hdr"> 启用 HDR (实验) </string>
<string name="summary_enable_hdr"> 当游戏和显卡支持时以HDR模式串流。 HDR需要GTX 1000系列或更高规格显卡。 </string>
<string name="title_enable_perf_overlay"> 启用性能信息 </string>
<string name="summary_enable_perf_overlay"> 在串流中显示实时性能信息 </string>
<string name="title_enable_post_stream_toast">串流完毕显示延迟信息</string>
<string name="summary_enable_post_stream_toast">串流结束后显示延迟信息</string>
<string name="title_osc_opacity">更改屏幕按钮透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>
+2 -18
View File
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight Root</string>
<!-- Shortcut strings -->
<string name="scut_deleted_pc"> 電腦已刪除 </string>
@@ -189,24 +187,10 @@
<string name="summary_video_format">H.265能降低視頻頻寬需求,但需要較新的設備才能支援</string>
<string name="title_enable_hdr"> 啟用 HDR (實驗) </string>
<string name="summary_enable_hdr"> 當遊戲和顯卡支援時以HDR模式串流。 HDR需要GTX 1000系列或更高規格顯卡。 </string>
<string name="title_enable_perf_overlay"> 啟用性能資訊 </string>
<string name="summary_enable_perf_overlay"> 在串流中顯示即時性能資訊 </string>
<string name="title_enable_post_stream_toast">串流完畢顯示延遲資訊</string>
<string name="summary_enable_post_stream_toast">串流結束後顯示延遲資訊</string>
<string name="title_osc_opacity">更改屏幕按鈕透明度</string>
<string name="dialog_title_osc_opacity">透明度</string>
<string name="suffix_osc_opacity">%</string>
+2
View File
@@ -55,6 +55,7 @@
<item>Français</item>
<item>Deutsch</item>
<item>Română</item>
<item>Українська</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
@@ -70,6 +71,7 @@
<item>fr</item>
<item>de</item>
<item>ro</item>
<item>uk</item>
</string-array>
<string-array name="decoder_names">
@@ -0,0 +1,3 @@
- Removed old button emulation feature which prevented LB+Start, RB+Start, and Start+Select from working as expected
- Fixed edges of on-screen analog sticks being clipped
- Updated Simplified and Traditional Chinese translations
@@ -0,0 +1,4 @@
- Added Ukrainian translation
- Fixed SSL error connecting to GFE 3.20.4 on Android 4.1-4.4
- Fixed a random crash when starting a stream in bad network conditions
- Fixed decoder-related crashes on Xiaomi Mi 10 Lite 5G devices