Compare commits

...

27 Commits

Author SHA1 Message Date
Cameron Gutman 37cf572c0c Increase version number 2014-06-26 20:39:31 -07:00
Cameron Gutman 33c5254d6f Yet again raise the minimum button down time based on testing on Ouya 2014-06-26 20:38:37 -07:00
Cameron Gutman faa82ca9d6 Fixup num_ref_frames in SPS on Qualcomm devices to (hopefully) fix the crashing video bug on the Galaxy S3 after Android 4.3 2014-06-26 20:03:12 -07:00
Cameron Gutman 0d35ea5207 Enable max Ethernet MTU sized packets when the remote PC is on the subnet 2014-06-22 17:08:22 -07:00
Cameron Gutman 579645c07c Make the pairing toast shorter. Display a nice message when we get a 404 instead of a huge URL. 2014-06-22 13:24:14 -07:00
Cameron Gutman 869cbe2e81 Frame latency and jitter improvements 2014-06-19 19:13:16 -07:00
Cameron Gutman 329a938bf8 Hold a high performance Wi-Fi lock while streaming 2014-06-19 18:26:33 -07:00
Cameron Gutman 411931cc27 Increment version and update common jar 2014-06-15 21:10:42 -07:00
Cameron Gutman ce01223683 Implement crypto provider and GFE 2.1 compatibility 2014-06-15 20:17:09 -07:00
Cameron Gutman e7501a488d Raise the minimum button down threshold to minimize missed start button presses on OUYA 2014-06-01 17:11:40 -04:00
Cameron Gutman 5626e9663b Fix Ouya controller combos and bump the version to 2.2.1.2 2014-05-31 16:33:51 -04:00
Cameron Gutman 01b35ccdd3 Controller handling improvements for Ouya 2014-05-27 23:45:30 -04:00
Cameron Gutman e83bc747c8 Add assets and manifest updates for OUYA store 2014-05-24 20:28:38 -04:00
Cameron Gutman cbe4af7623 Remove extra files from TinyRTSP Jar 2014-05-12 20:36:07 -05:00
Cameron Gutman fc9e45270a Bump version number up 2014-05-12 20:06:27 -05:00
Cameron Gutman 94c1fc2b66 Update common 2014-05-12 20:06:12 -05:00
Cameron Gutman 49999634c1 Lower floors even more since we're clamping min = max 2014-05-12 20:04:33 -05:00
Cameron Gutman 09f4827d02 Update TinyRTSP Jar to work on Java 1.6 2014-05-11 13:41:29 -04:00
Cameron Gutman 52e4e81e35 Adjust bitrate floors and defaults based on user feedback 2014-05-11 13:32:49 -04:00
Cameron Gutman 56b752f63f Fix dialog rundown crashes 2014-05-07 22:41:41 -04:00
Cameron Gutman 2e6e835a8e Update bitrate label to be more intuitive 2014-05-07 22:41:02 -04:00
Cameron Gutman d8b0a0ffb5 Style changes to compress the UI a bit 2014-05-07 22:29:39 -04:00
Cameron Gutman b82d74474a Behave better when in immersive mode. Take up the entire screen, instead of leaving the navigation bar empty. Re-enter immersive mode if the volume buttons are pressed 2014-05-07 22:17:16 -04:00
Cameron Gutman 508b855e36 Save the bitrate preference before starting a stream 2014-05-07 21:39:23 -04:00
Cameron Gutman 5efcd606e3 Increment version to 2.2 2014-05-07 02:12:21 -04:00
Cameron Gutman 3524cdd764 Add TinyRTSP Jar 2014-05-07 02:12:07 -04:00
Cameron Gutman 368cd8808d Add support for selecting stream bitrate. 2014-05-07 02:11:10 -04:00
19 changed files with 757 additions and 154 deletions
+4 -2
View File
@@ -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="12"
android:versionName="2.1.6" >
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
+15 -10
View File
@@ -32,21 +32,26 @@ 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=0x7f080005;
public static final int config1080p30Selected=0x7f080009;
public static final int config1080p60Selected=0x7f08000a;
public static final int config720p30Selected=0x7f080007;
public static final int config720p60Selected=0x7f080008;
public static final int hardwareDec=0x7f080006;
public static final int autoDec=0x7f080006;
public static final int bitrateLabel=0x7f08000c;
public static final int bitrateSeekBar=0x7f08000d;
public static final int config1080p30Selected=0x7f08000a;
public static final int config1080p60Selected=0x7f08000b;
public static final int config720p30Selected=0x7f080008;
public static final int config720p60Selected=0x7f080009;
public static final int decoderConfigGroup=0x7f080003;
public static final int hardwareDec=0x7f080007;
public static final int hostTextView=0x7f080000;
public static final int pairButton=0x7f080002;
public static final int softwareDec=0x7f080004;
public static final int softwareDec=0x7f080005;
public static final int statusButton=0x7f080001;
public static final int streamConfigGroup=0x7f080003;
public static final int surfaceView=0x7f08000b;
public static final int streamConfigGroup=0x7f080004;
public static final int surfaceView=0x7f08000e;
}
public static final class layout {
public static final int activity_connection=0x7f030000;
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
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

+25 -6
View File
@@ -5,7 +5,7 @@
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingTop="10dp"
tools:context=".Connection" >
<RelativeLayout
@@ -45,12 +45,13 @@
android:text="Pair with PC" />
<RadioGroup
android:id="@+id/decoderConfigGroup"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/streamConfigGroup"
android:layout_marginTop="25dp"
android:layout_marginTop="15dp"
android:orientation="vertical" >
<RadioButton
@@ -79,7 +80,7 @@
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/pairButton"
android:layout_marginTop="25dp"
android:layout_marginTop="10dp"
android:orientation="vertical" >
<RadioButton
@@ -92,24 +93,42 @@
android:id="@+id/config720p60Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginTop="7dp"
android:text="720p 60 FPS (Recommended for most devices and networks)" />
<RadioButton
android:id="@+id/config1080p30Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginTop="7dp"
android:text="1080p 30 FPS (Recommended for most devices if 1080p streaming is desired)" />
<RadioButton
android:id="@+id/config1080p60Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginTop="7dp"
android:text="1080p 60 FPS (Requires extremely fast device and network)" />
</RadioGroup>
<TextView
android:id="@+id/bitrateLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:layout_alignParentRight="true"
android:layout_below="@+id/decoderConfigGroup" />
<SeekBar
android:id="@+id/bitrateSeekBar"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/decoderConfigGroup"
android:layout_toLeftOf="@+id/bitrateLabel" />
</RelativeLayout>
</ScrollView>
+111 -25
View File
@@ -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;
@@ -20,6 +19,8 @@ import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import android.app.Activity;
@@ -33,6 +34,8 @@ public class Connection extends Activity {
private SharedPreferences prefs;
private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
private RadioButton forceSoftDec, autoDec, forceHardDec;
private SeekBar bitrateSlider;
private TextView bitrateLabel;
private static final String DEFAULT_HOST = "";
public static final String HOST_KEY = "hostText";
@@ -42,11 +45,19 @@ public class Connection extends Activity {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(Connection.HOST_KEY, this.hostText.getText().toString());
editor.putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress());
editor.apply();
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -66,12 +77,18 @@ public class Connection extends Activity {
this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
this.autoDec = (RadioButton) findViewById(R.id.autoDec);
this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
this.bitrateLabel = (TextView) findViewById(R.id.bitrateLabel);
this.bitrateSlider = (SeekBar) findViewById(R.id.bitrateSeekBar);
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST));
boolean res720p = prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720;
boolean fps30 = prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30;
bitrateSlider.setMax(Game.BITRATE_CEILING);
bitrateSlider.setProgress(prefs.getInt(Game.BITRATE_PREF_STRING, Game.DEFAULT_BITRATE));
updateBitrateLabel();
rbutton720p30.setChecked(false);
rbutton720p60.setChecked(false);
@@ -124,22 +141,30 @@ public class Connection extends Activity {
if (buttonView == rbutton720p30) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
putInt(Game.HEIGHT_PREF_STRING, 720).
putInt(Game.REFRESH_RATE_PREF_STRING, 30).commit();
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_30).commit();
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_720_30);
}
else if (buttonView == rbutton720p60) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
putInt(Game.HEIGHT_PREF_STRING, 720).
putInt(Game.REFRESH_RATE_PREF_STRING, 60).commit();
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_60).commit();
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_720_60);
}
else if (buttonView == rbutton1080p30) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
putInt(Game.HEIGHT_PREF_STRING, 1080).
putInt(Game.REFRESH_RATE_PREF_STRING, 30).commit();
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_30).commit();
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_1080_30);
}
else if (buttonView == rbutton1080p60) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
putInt(Game.HEIGHT_PREF_STRING, 1080).
putInt(Game.REFRESH_RATE_PREF_STRING, 60).commit();
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_60).commit();
bitrateSlider.setProgress(Game.BITRATE_DEFAULT_1080_60);
}
else if (buttonView == forceSoftDec) {
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
@@ -160,6 +185,45 @@ public class Connection extends Activity {
forceHardDec.setOnCheckedChangeListener(occl);
autoDec.setOnCheckedChangeListener(occl);
this.bitrateSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// Verify the user's selection
if (fromUser) {
int floor;
if (rbutton720p30.isChecked()) {
floor = Game.BITRATE_FLOOR_720_30;
}
else if (rbutton720p60.isChecked()){
floor = Game.BITRATE_FLOOR_720_60;
}
else if (rbutton1080p30.isChecked()){
floor = Game.BITRATE_FLOOR_1080_30;
}
else /*if (rbutton1080p60.isChecked())*/ {
floor = Game.BITRATE_FLOOR_1080_60;
}
if (progress < floor) {
seekBar.setProgress(floor);
return;
}
}
updateBitrateLabel();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
this.statusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
@@ -168,6 +232,12 @@ public class Connection extends Activity {
return;
}
// Ensure that the bitrate preference is up to date before
// starting the game activity
prefs.edit().
putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress()).
commit();
Intent intent = new Intent(Connection.this, Game.class);
intent.putExtra("host", Connection.this.hostText.getText().toString());
Connection.this.startActivity(intent);
@@ -182,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() {
@@ -203,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
@@ -240,4 +323,7 @@ public class Connection extends Activity {
}
private void updateBitrateLabel() {
bitrateLabel.setText("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
}
}
+178 -11
View File
@@ -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,7 +26,10 @@ 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;
import android.view.InputDevice;
import android.view.KeyEvent;
@@ -55,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";
@@ -63,11 +75,25 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
public static final String HEIGHT_PREF_STRING = "ResV";
public static final String REFRESH_RATE_PREF_STRING = "FPS";
public static final String DECODER_PREF_STRING = "Decoder";
public static final String BITRATE_PREF_STRING = "Bitrate";
public static final int BITRATE_FLOOR_720_30 = 2;
public static final int BITRATE_FLOOR_720_60 = 4;
public static final int BITRATE_FLOOR_1080_30 = 4;
public static final int BITRATE_FLOOR_1080_60 = 10;
public static final int BITRATE_DEFAULT_720_30 = 5;
public static final int BITRATE_DEFAULT_720_60 = 10;
public static final int BITRATE_DEFAULT_1080_30 = 10;
public static final int BITRATE_DEFAULT_1080_60 = 30;
public static final int BITRATE_CEILING = 50;
public static final int DEFAULT_WIDTH = 1280;
public static final int DEFAULT_HEIGHT = 720;
public static final int DEFAULT_REFRESH_RATE = 60;
public static final int DEFAULT_DECODER = 0;
public static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
@@ -77,15 +103,24 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Full-screen and don't let the display go off
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// If we're going to use immersive mode, we want to have
// the entire screen
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
}
// Change volume button behavior
setVolumeControlStream(AudioManager.STREAM_MUSIC);
@@ -116,10 +151,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
break;
}
int refreshRate;
int refreshRate, bitrate;
width = prefs.getInt(WIDTH_PREF_STRING, DEFAULT_WIDTH);
height = prefs.getInt(HEIGHT_PREF_STRING, DEFAULT_HEIGHT);
refreshRate = prefs.getInt(REFRESH_RATE_PREF_STRING, DEFAULT_REFRESH_RATE);
bitrate = prefs.getInt(BITRATE_PREF_STRING, DEFAULT_BITRATE);
sh.setFixedSize(width, height);
Display display = getWindowManager().getDefaultDisplay();
@@ -127,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));
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);
@@ -138,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);
@@ -147,8 +291,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
}
@SuppressLint("InlinedApi")
private void hideSystemUi() {
runOnUiThread(new Runnable() {
private Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds
@@ -167,7 +310,14 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
}
});
};
private void hideSystemUi() {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.removeCallbacks(hideSystemUi);
h.postDelayed(hideSystemUi, 1000);
}
}
@Override
@@ -183,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()) {
@@ -220,6 +377,16 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Pressing a volume button drops the immersive flag so the UI shows up again and doesn't
// go away. I'm not sure if that's a bug or a feature, but we're working around it here
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.removeCallbacks(hideSystemUi);
h.postDelayed(hideSystemUi, 2000);
}
}
if (event.getDevice() != null &&
(event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) {
short translated = keybTranslator.translate(event.getKeyCode());
@@ -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;
}
/**
+5 -2
View File
@@ -25,8 +25,11 @@ public class Dialog implements Runnable {
public static void closeDialogs()
{
for (Dialog d : rundownDialogs)
d.alert.dismiss();
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
+8 -3
View File
@@ -33,8 +33,11 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
public static void closeDialogs()
{
for (SpinnerDialog d : rundownDialogs)
d.progress.dismiss();
for (SpinnerDialog d : rundownDialogs) {
if (d.progress.isShowing()) {
d.progress.dismiss();
}
}
rundownDialogs.clear();
}
@@ -86,7 +89,9 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
}
else
{
progress.dismiss();
if (progress.isShowing()) {
progress.dismiss();
}
}
}