Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37cf572c0c | |||
| 33c5254d6f | |||
| faa82ca9d6 | |||
| 0d35ea5207 | |||
| 579645c07c | |||
| 869cbe2e81 | |||
| 329a938bf8 | |||
| 411931cc27 | |||
| ce01223683 | |||
| e7501a488d | |||
| 5626e9663b | |||
| 01b35ccdd3 | |||
| e83bc747c8 |
+4
-2
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.limelight"
|
||||
android:versionCode="14"
|
||||
android:versionName="2.2.1" >
|
||||
android:versionCode="18"
|
||||
android:versionName="2.4" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
@@ -24,6 +25,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.APP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
|
||||
@@ -32,7 +32,9 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
|
||||
public static final int activity_vertical_margin=0x7f050001;
|
||||
}
|
||||
public static final class drawable {
|
||||
public static final int ic_launcher=0x7f020000;
|
||||
public static final int app_icon=0x7f020000;
|
||||
public static final int ic_launcher=0x7f020001;
|
||||
public static final int ouya_icon=0x7f020002;
|
||||
}
|
||||
public static final class id {
|
||||
public static final int autoDec=0x7f080006;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -1,16 +1,15 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.utils.Dialog;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
@@ -52,6 +51,13 @@ public class Connection extends Activity {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
Dialog.closeDialogs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -246,7 +252,7 @@ public class Connection extends Activity {
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_SHORT).show();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -267,29 +273,42 @@ public class Connection extends Activity {
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(InetAddress.getByName(hostText.getText().toString()),
|
||||
macAddress, PlatformBinding.getDeviceName());
|
||||
try {
|
||||
if (httpConn.getPairState()) {
|
||||
message = "Already paired";
|
||||
macAddress, PlatformBinding.getDeviceName(), PlatformBinding.getCryptoProvider(Connection.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
message = "Already paired";
|
||||
}
|
||||
else {
|
||||
final String pinStr = PairingManager.generatePinString();
|
||||
|
||||
// Spin the dialog off in a thread because it blocks
|
||||
Dialog.displayDialog(Connection.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
|
||||
|
||||
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
message = "Incorrect PIN";
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.FAILED) {
|
||||
message = "Pairing failed";
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||
message = "Paired successfully";
|
||||
}
|
||||
else {
|
||||
int session = httpConn.getSessionId();
|
||||
if (session == 0) {
|
||||
message = "Pairing was declined by the target";
|
||||
}
|
||||
else {
|
||||
message = "Pairing was successful";
|
||||
}
|
||||
// Should be no other values
|
||||
message = null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
message = e.getMessage();
|
||||
} catch (XmlPullParserException e) {
|
||||
message = e.getMessage();
|
||||
}
|
||||
} catch (UnknownHostException e1) {
|
||||
} catch (UnknownHostException e) {
|
||||
message = "Failed to resolve host";
|
||||
} catch (FileNotFoundException e) {
|
||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||
+ "Try rebooting your machine or reinstalling GFE.";
|
||||
} catch (Exception e) {
|
||||
message = e.getMessage();
|
||||
}
|
||||
|
||||
Dialog.closeDialogs();
|
||||
|
||||
final String toastMessage = message;
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
|
||||
+128
-2
@@ -1,5 +1,12 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.binding.input.ControllerHandler;
|
||||
import com.limelight.binding.input.KeyboardTranslator;
|
||||
@@ -19,6 +26,8 @@ import android.content.SharedPreferences;
|
||||
import android.graphics.Point;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Display;
|
||||
@@ -56,6 +65,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
||||
private boolean connecting = false;
|
||||
private boolean connected = false;
|
||||
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
|
||||
private int drFlags = 0;
|
||||
|
||||
public static final String PREFS_FILE_NAME = "gameprefs";
|
||||
@@ -152,10 +163,35 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
||||
|
||||
// Warn the user if they're on a metered connection
|
||||
checkDataConnection();
|
||||
|
||||
// Make sure Wi-Fi is fully powered up
|
||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
||||
wifiLock.setReferenceCounted(false);
|
||||
wifiLock.acquire();
|
||||
|
||||
String host = Game.this.getIntent().getStringExtra("host");
|
||||
InetAddress addr;
|
||||
boolean enableLargePackets;
|
||||
try {
|
||||
// If this does a DNS lookup, it could cause a NetworkOnMainThread exception
|
||||
// Chances are if it has to do this, we're not on the same network anyways
|
||||
addr = InetAddress.getByName(host);
|
||||
|
||||
// Check if we can enable large packets if we get this far
|
||||
enableLargePackets = shouldEnableLargePackets(addr);
|
||||
} catch (Exception e) {
|
||||
// We don't want to deal with any exceptions here. The user will be notified
|
||||
// when the connection fails
|
||||
enableLargePackets = false;
|
||||
}
|
||||
|
||||
LimeLog.info("Using large packets? "+enableLargePackets);
|
||||
|
||||
// Start the connection
|
||||
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
|
||||
new StreamConfiguration(width, height, refreshRate, bitrate * 1000));
|
||||
conn = new NvConnection(host, Game.this,
|
||||
new StreamConfiguration(width, height, refreshRate, bitrate * 1000,
|
||||
enableLargePackets ? 1460 : 1024), PlatformBinding.getCryptoProvider(this));
|
||||
keybTranslator = new KeyboardTranslator(conn);
|
||||
controllerHandler = new ControllerHandler(conn);
|
||||
|
||||
@@ -163,6 +199,89 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
||||
sh.addCallback(this);
|
||||
}
|
||||
|
||||
private boolean shouldEnableLargePackets(InetAddress targetAddress) {
|
||||
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||
String matchingPrefix;
|
||||
|
||||
if (activeNetworkInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (activeNetworkInfo.getType())
|
||||
{
|
||||
case ConnectivityManager.TYPE_ETHERNET:
|
||||
matchingPrefix = "eth";
|
||||
break;
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
matchingPrefix = "wlan";
|
||||
break;
|
||||
|
||||
default:
|
||||
// Must be on Ethernet or Wifi to consider that we can send large packets
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to find the interface that corresponds to the active network
|
||||
try {
|
||||
Enumeration<NetworkInterface> ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||
while (ifaceList.hasMoreElements()) {
|
||||
NetworkInterface iface = ifaceList.nextElement();
|
||||
|
||||
// Look for an interface that matches the prefix we expect
|
||||
if (iface.isUp() && iface.getName().startsWith(matchingPrefix)) {
|
||||
// Find the IPv4 address for the interface
|
||||
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
||||
if (!(addr.getAddress() instanceof Inet4Address)) {
|
||||
// Skip non-IPv4 addresses
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found the right address on the right interface
|
||||
return isOnSameSubnet(targetAddress, addr.getAddress(), addr.getNetworkPrefixLength());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// We didn't find the interface or something else went wrong
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isOnSameSubnet(InetAddress targetAddress, InetAddress localAddress, short networkPrefixLength) {
|
||||
byte[] targetBytes = targetAddress.getAddress();
|
||||
byte[] localBytes = localAddress.getAddress();
|
||||
|
||||
for (int byteIndex = 0; networkPrefixLength > 0; byteIndex++) {
|
||||
byte target = targetBytes[byteIndex];
|
||||
byte local = localBytes[byteIndex];
|
||||
|
||||
if (networkPrefixLength >= 8) {
|
||||
// Do a full byte comparison
|
||||
if (target != local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
networkPrefixLength -= 8;
|
||||
}
|
||||
else {
|
||||
target &= (byte)(0xFF << (8 - networkPrefixLength));
|
||||
local &= (byte)(0xFF << (8 - networkPrefixLength));
|
||||
|
||||
// Do a masked comparison
|
||||
if (target != local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
networkPrefixLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void checkDataConnection()
|
||||
{
|
||||
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
@@ -214,6 +333,13 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
wifiLock.release();
|
||||
}
|
||||
|
||||
private static byte getModifierState(KeyEvent event) {
|
||||
byte modifier = 0;
|
||||
if (event.isShiftPressed()) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.limelight.binding;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
public class PlatformBinding {
|
||||
public static String getDeviceName() {
|
||||
@@ -13,4 +17,8 @@ public class PlatformBinding {
|
||||
public static AudioRenderer getAudioRenderer() {
|
||||
return new AndroidAudioRenderer();
|
||||
}
|
||||
|
||||
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||
return new AndroidCryptoProvider(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.limelight.binding.crypto;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMWriter;
|
||||
import org.bouncycastle.x509.X509V3CertificateGenerator;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
private File certFile;
|
||||
private File keyFile;
|
||||
|
||||
private X509Certificate cert;
|
||||
private RSAPrivateKey key;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
static {
|
||||
// Install the Bouncy Castle provider
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public AndroidCryptoProvider(Context c) {
|
||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||
|
||||
certFile = new File(dataPath + File.separator + "client.crt");
|
||||
keyFile = new File(dataPath + File.separator + "client.key");
|
||||
}
|
||||
|
||||
private byte[] loadFileToBytes(File f) {
|
||||
if (!f.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(f);
|
||||
byte[] fileData = new byte[(int) f.length()];
|
||||
fin.read(fileData);
|
||||
fin.close();
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadCertKeyPair() {
|
||||
byte[] certBytes = loadFileToBytes(certFile);
|
||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||
|
||||
// If either file was missing, we definitely can't succeed
|
||||
if (certBytes == null || keyBytes == null) {
|
||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
pemCertBytes = certBytes;
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
} catch (CertificateException e) {
|
||||
// May happen if the cert is corrupt
|
||||
LimeLog.warning("Corrupted certificate");
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
// May happen if the key is corrupt
|
||||
LimeLog.warning("Corrupted key");
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Loaded key pair from disk");
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("TrulyRandom")
|
||||
private boolean generateCertKeyPair() {
|
||||
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
|
||||
X500Principal principalName = new X500Principal("CN=NVIDIA GameStream Client");
|
||||
|
||||
byte[] snBytes = new byte[8];
|
||||
new SecureRandom().nextBytes(snBytes);
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e1) {
|
||||
// Should never happen
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
Date expirationDate = new Date();
|
||||
|
||||
// Expires in 20 years
|
||||
expirationDate.setYear(expirationDate.getYear() + 20);
|
||||
|
||||
certGenerator.setSerialNumber(new BigInteger(snBytes).abs());
|
||||
certGenerator.setIssuerDN(principalName);
|
||||
certGenerator.setNotBefore(now);
|
||||
certGenerator.setNotAfter(expirationDate);
|
||||
certGenerator.setSubjectDN(principalName);
|
||||
certGenerator.setPublicKey(keyPair.getPublic());
|
||||
certGenerator.setSignatureAlgorithm("SHA1withRSA");
|
||||
|
||||
try {
|
||||
cert = certGenerator.generate(keyPair.getPrivate(), "BC");
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
// Save the resulting pair
|
||||
saveCertKeyPair();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveCertKeyPair() {
|
||||
try {
|
||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||
|
||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||
StringWriter strWriter = new StringWriter();
|
||||
PEMWriter pemWriter = new PEMWriter(strWriter);
|
||||
pemWriter.writeObject(cert);
|
||||
pemWriter.close();
|
||||
|
||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||
String pemStr = strWriter.getBuffer().toString();
|
||||
for (int i = 0; i < pemStr.length(); i++) {
|
||||
char c = pemStr.charAt(i);
|
||||
if (c != '\r')
|
||||
certWriter.append(c);
|
||||
}
|
||||
certWriter.close();
|
||||
|
||||
// Write the private out in PKCS8 format
|
||||
keyOut.write(key.getEncoded());
|
||||
|
||||
certOut.close();
|
||||
keyOut.close();
|
||||
|
||||
LimeLog.info("Saved generated key pair to disk");
|
||||
} catch (IOException e) {
|
||||
// This isn't good because it means we'll have
|
||||
// to re-pair next time
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public X509Certificate getClientCertificate() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (this) {
|
||||
// Return a loaded cert if we have one
|
||||
if (cert != null) {
|
||||
return cert;
|
||||
}
|
||||
|
||||
// No loaded cert yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return cert;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
|
||||
public RSAPrivateKey getClientPrivateKey() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (this) {
|
||||
// Return a loaded key if we have one
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// No loaded key yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return key;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getPemEncodedClientCertificate() {
|
||||
synchronized (this) {
|
||||
// Call our helper function to do the cert loading/generation for us
|
||||
getClientCertificate();
|
||||
|
||||
// Return a cached value if we have it
|
||||
return pemCertBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encodeBase64String(byte[] data) {
|
||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.limelight.binding.input;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
@@ -17,6 +18,24 @@ public class ControllerHandler {
|
||||
private short rightStickY = 0x0000;
|
||||
private short leftStickX = 0x0000;
|
||||
private short leftStickY = 0x0000;
|
||||
private 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
|
||||
// a Start button press to see if we should activate one of our
|
||||
// emulated button combos.
|
||||
private long lastLbUpTime = 0;
|
||||
private long lastRbUpTime = 0;
|
||||
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||
|
||||
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 HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||
|
||||
@@ -298,6 +317,18 @@ public class ControllerHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the button hasn't been down long enough, sleep for a bit before sending the up event
|
||||
// This allows "instant" button presses (like OUYA's virtual menu button) to work. This
|
||||
// path should not be triggered during normal usage.
|
||||
if (SystemClock.uptimeMillis() - event.getDownTime() < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
|
||||
{
|
||||
// Since our sleep time is so short (10 ms), it shouldn't cause a problem doing this in the
|
||||
// UI thread.
|
||||
try {
|
||||
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
@@ -337,9 +368,11 @@ public class ControllerHandler {
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||
lastLbUpTime = SystemClock.uptimeMillis();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||
lastRbUpTime = SystemClock.uptimeMillis();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||
@@ -357,11 +390,39 @@ public class ControllerHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If one of the two is up, the special button comes up too
|
||||
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
|
||||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
|
||||
// Check if we're emulating the select button
|
||||
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||
{
|
||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
// If either start or LB is up, select comes up too
|
||||
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
(inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||
{
|
||||
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
|
||||
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're emulating the special button
|
||||
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||
{
|
||||
// If either start or select and RB is up, the special button comes up too
|
||||
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||
(inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||
{
|
||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
sendControllerInputPacket();
|
||||
@@ -438,12 +499,27 @@ public class ControllerHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We detect back+start as the special button combo
|
||||
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
|
||||
// Start+LB acts like select for controllers with one button
|
||||
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||
{
|
||||
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||
inputMap |= ControllerPacket.BACK_FLAG;
|
||||
|
||||
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||
}
|
||||
|
||||
// We detect select+start or start+RB as the special button combo
|
||||
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||
{
|
||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
|
||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
}
|
||||
|
||||
sendControllerInputPacket();
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||
|
||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
@@ -141,22 +142,23 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
public void start(final VideoDepacketizer depacketizer) {
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = System.currentTimeMillis();
|
||||
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
if (du != null) {
|
||||
submitDecodeUnit(du);
|
||||
}
|
||||
|
||||
long diff = nextFrameTime - System.currentTimeMillis();
|
||||
|
||||
if (diff > WAIT_CEILING_MS) {
|
||||
try {
|
||||
Thread.sleep(diff);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
@@ -165,6 +167,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (CPU)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
@@ -186,8 +189,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
|
||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
@@ -26,8 +26,8 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
decoderRenderer.start();
|
||||
public void start(VideoDepacketizer depacketizer) {
|
||||
decoderRenderer.start(depacketizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,11 +35,6 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
decoderRenderer.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit du) {
|
||||
return decoderRenderer.submitDecodeUnit(du);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return decoderRenderer.getCapabilities();
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
@@ -23,15 +24,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
private ByteBuffer[] videoDecoderInputBuffers;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
private int redrawRate;
|
||||
private boolean needsSpsBitstreamFixup;
|
||||
private boolean needsSpsNumRefFixup;
|
||||
private boolean fastInputQueueing;
|
||||
private VideoDepacketizer depacketizer;
|
||||
|
||||
public static final List<String> blacklistedDecoderPrefixes;
|
||||
public static final List<String> spsFixupBitsreamFixupDecoderPrefixes;
|
||||
public static final List<String> spsFixupNumRefFixupDecoderPrefixes;
|
||||
public static final List<String> fastInputQueueingPrefixes;
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||
@@ -45,11 +44,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
spsFixupNumRefFixupDecoderPrefixes = new LinkedList<String>();
|
||||
spsFixupNumRefFixupDecoderPrefixes.add("omx.TI");
|
||||
}
|
||||
|
||||
static {
|
||||
fastInputQueueingPrefixes = new LinkedList<String>();
|
||||
fastInputQueueingPrefixes.add("omx.nvidia");
|
||||
spsFixupNumRefFixupDecoderPrefixes.add("omx.qcom");
|
||||
}
|
||||
|
||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||
@@ -124,9 +119,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.redrawRate = redrawRate;
|
||||
|
||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
//dumpDecoders();
|
||||
|
||||
MediaCodecInfo safeDecoder = findSafeDecoder();
|
||||
@@ -140,16 +133,11 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
if (needsSpsNumRefFixup) {
|
||||
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
|
||||
}
|
||||
fastInputQueueing = isDecoderInList(fastInputQueueingPrefixes, safeDecoder.getName());
|
||||
if (fastInputQueueing) {
|
||||
LimeLog.info("Decoder "+safeDecoder.getName()+" supports fast input queueing");
|
||||
}
|
||||
}
|
||||
else {
|
||||
videoDecoder = MediaCodec.createDecoderByType("video/avc");
|
||||
needsSpsBitstreamFixup = false;
|
||||
needsSpsNumRefFixup = false;
|
||||
fastInputQueueing = false;
|
||||
}
|
||||
|
||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
||||
@@ -167,59 +155,51 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTimeUs = 0;
|
||||
BufferInfo info = new BufferInfo();
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
// Block for a maximum of 100 ms
|
||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100000);
|
||||
switch (outIndex) {
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
LimeLog.info("Output buffers changed");
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
if (du != null) {
|
||||
submitDecodeUnit(du);
|
||||
}
|
||||
|
||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
|
||||
if (outIndex >= 0) {
|
||||
int lastIndex = outIndex;
|
||||
boolean render = false;
|
||||
|
||||
if (currentTimeUs() >= nextFrameTimeUs) {
|
||||
render = true;
|
||||
nextFrameTimeUs = computePresentationTime(redrawRate);
|
||||
}
|
||||
|
||||
// Get the last output buffer in the queue
|
||||
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||
lastIndex = outIndex;
|
||||
}
|
||||
|
||||
// Render that buffer if it's time for the next frame
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, render);
|
||||
|
||||
// Render the last buffer
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
} else {
|
||||
switch (outIndex) {
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
LimeLog.info("Output buffers changed");
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
private static long currentTimeUs() {
|
||||
return System.nanoTime() / 1000;
|
||||
}
|
||||
|
||||
private long computePresentationTime(int frameRate) {
|
||||
return currentTimeUs() + (1000000 / frameRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
public void start(VideoDepacketizer depacketizer) {
|
||||
this.depacketizer = depacketizer;
|
||||
startRendererThread();
|
||||
}
|
||||
|
||||
@@ -239,24 +219,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
if (decodeUnit.getType() != DecodeUnit.TYPE_H264) {
|
||||
System.err.println("Unknown decode unit type");
|
||||
return false;
|
||||
}
|
||||
|
||||
int mcFlags = 0;
|
||||
|
||||
if ((decodeUnit.getFlags() & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||
LimeLog.info("Codec config");
|
||||
mcFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||
}
|
||||
if ((decodeUnit.getFlags() & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
|
||||
LimeLog.info("Sync frame");
|
||||
mcFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
||||
}
|
||||
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||
if (inputIndex >= 0)
|
||||
{
|
||||
@@ -272,7 +235,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
if (needsSpsNumRefFixup) {
|
||||
LimeLog.info("Fixing up num ref frames");
|
||||
this.replace(header, 80, 9, new byte[] {0x40}, 3);
|
||||
}
|
||||
}
|
||||
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||
@@ -312,7 +275,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
videoDecoder.queueInputBuffer(inputIndex,
|
||||
0, spsLength,
|
||||
0, mcFlags);
|
||||
0, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -325,7 +288,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
videoDecoder.queueInputBuffer(inputIndex,
|
||||
0, decodeUnit.getDataLength(),
|
||||
0, mcFlags);
|
||||
0, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -333,7 +296,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return fastInputQueueing ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user