mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 10:31:07 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc7ac79fa6 | ||
|
|
4c5c27dfc1 | ||
|
|
4aabfbd52e | ||
|
|
6eab842361 | ||
|
|
b729dfd702 | ||
|
|
6366840781 | ||
|
|
704a2ee90b | ||
|
|
484be9bfe6 | ||
|
|
a99e070c26 | ||
|
|
bf803f88af | ||
|
|
9af6febca5 | ||
|
|
0101d0a1bd | ||
|
|
266874609d | ||
|
|
2ba7feedfc | ||
|
|
43c67b4939 | ||
|
|
2d9915e43a | ||
|
|
2329b41bce | ||
|
|
536496184e |
@@ -7,8 +7,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
|
||||
versionName "9.6.1"
|
||||
versionCode = 233
|
||||
versionName "9.6.4"
|
||||
versionCode = 237
|
||||
}
|
||||
|
||||
flavorDimensions "root"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1616,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Submodule app/src/main/jni/moonlight-core/moonlight-common-c updated: 407d57a50a...5c9935e11b
14
app/src/main/res/values-uk/arrays.xml
Normal file
14
app/src/main/res/values-uk/arrays.xml
Normal 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
app/src/main/res/values-uk/strings.xml
Normal file
195
app/src/main/res/values-uk/strings.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/237.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/237.txt
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user