Compare commits

..

36 Commits

Author SHA1 Message Date
Cameron Gutman 65b492f581 Submit codec config frames with the corresponding flag to work around an TI OMAP 4 errata. Also add a file that documents the currently known codec errata. 2014-07-02 23:42:54 -07:00
Cameron Gutman 8f43b95129 Remove the RenderScript renderer 2014-06-29 23:51:59 -07:00
Cameron Gutman faa2be431f Replace dnsjava with jmdns and remove a redundant classpath entry 2014-06-29 23:50:32 -07:00
Cameron Gutman 16de523e78 Ignore Lint error because Bouncy Castle calls API that don't exist in Android (some javax packages). We don't call these paths so we're okay now, but we need to remember that this can be a problem later on. 2014-06-29 12:49:10 -07:00
Cameron Gutman b24abc6ddd Update common for the app launch support 2014-06-29 12:40:22 -07:00
Cameron Gutman a450cd5b01 Remove deprecated crypto API usage based on irtimmer's changes to Limelight-PC 2014-06-29 12:39:36 -07:00
Cameron Gutman 2e3b7a2c09 Ignore Lint accessibility warning for onTouch handler 2014-06-29 12:32:10 -07:00
Cameron Gutman 22bba877d7 Ignore Lint warning for Amazon and Ouya assets 2014-06-29 12:12:11 -07:00
Cameron Gutman 7142db3fac Fixes for Android L and some weird codec exceptions 2014-06-29 11:49:42 -07:00
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
26 changed files with 832 additions and 211 deletions
-1
View File
@@ -5,6 +5,5 @@
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="lib" path="libs/limelight-common.jar"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
+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
+10
View File
@@ -0,0 +1,10 @@
This file serves to document some of the decoder errata when using MediaCodec hardware decoders on certain devices.
1. num_ref_frames is set to 16 by NVENC which causes decoders to allocate 16+ buffers. This can cause an OOM error on some devices.
- Affected decoders: TI OMAP4, possibly some Qualcomm chips too (Galaxy S3 on 4.3+)
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
- Affected decoders: TI OMAP4
+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.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="IconDuplicates">
<ignore path="res/drawable/app_icon.png" />
</issue>
<issue id="IconLocation">
<ignore path="res/drawable/app_icon.png" />
</issue>
<issue id="InvalidPackage">
<ignore path="libs/bcprov-jdk15on-150.jar" />
</issue>
<issue id="UnusedResources">
<ignore path="res/drawable-xhdpi/ouya_icon.png" />
<ignore path="res/drawable/app_icon.png" />
</issue>
</lint>
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");
}
}
+179 -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("Steam", 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());
@@ -398,6 +565,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM
return onGenericMotionEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
// Send it to the activity's touch event handler
@@ -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);
}
}
@@ -14,7 +14,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
private AudioTrack track;
@Override
public void streamInitialized(int channelCount, int sampleRate) {
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
@@ -27,7 +27,8 @@ public class AndroidAudioRenderer implements AudioRenderer {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
throw new IllegalArgumentException("Decoder returned unhandled channel count");
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
@@ -47,6 +48,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
AudioTrack.MODE_STREAM);
track.play();
return true;
}
@Override
@@ -0,0 +1,275 @@
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.Calendar;
import java.util.Date;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import android.content.Context;
import android.util.Base64;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.LimelightCryptoProvider;
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;
}
private boolean generateCertKeyPair() {
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();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(name, serial, now, expirationDate, name, keyPair.getPublic());
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
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 {
@@ -80,7 +81,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
}
@Override
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = findOptimalPerformanceLevel();
@@ -138,25 +139,28 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override
public void start() {
public boolean 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,7 +169,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
private long computePresentationTimeMs(int frameRate) {
@@ -186,8 +192,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 {
@@ -13,7 +13,7 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
}
@Override
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecDecoderRenderer.findSafeDecoder() != null)) {
@@ -22,12 +22,12 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
}
@Override
public void start() {
decoderRenderer.start();
public boolean start(VideoDepacketizer depacketizer) {
return 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,32 +119,32 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
}
@Override
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.redrawRate = redrawRate;
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
//dumpDecoders();
MediaCodecInfo safeDecoder = findSafeDecoder();
if (safeDecoder != null) {
videoDecoder = MediaCodec.createByCodecName(safeDecoder.getName());
needsSpsBitstreamFixup = isDecoderInList(spsFixupBitsreamFixupDecoderPrefixes, safeDecoder.getName());
needsSpsNumRefFixup = isDecoderInList(spsFixupNumRefFixupDecoderPrefixes, safeDecoder.getName());
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS bitstream restrictions fixup");
// It's nasty to put all this in a try-catch block,
// but codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems
try {
MediaCodecInfo safeDecoder = findSafeDecoder();
if (safeDecoder != null) {
videoDecoder = MediaCodec.createByCodecName(safeDecoder.getName());
needsSpsBitstreamFixup = isDecoderInList(spsFixupBitsreamFixupDecoderPrefixes, safeDecoder.getName());
needsSpsNumRefFixup = isDecoderInList(spsFixupNumRefFixupDecoderPrefixes, safeDecoder.getName());
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS bitstream restrictions fixup");
}
if (needsSpsNumRefFixup) {
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
}
}
if (needsSpsNumRefFixup) {
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
else {
videoDecoder = MediaCodec.createDecoderByType("video/avc");
needsSpsBitstreamFixup = false;
needsSpsNumRefFixup = false;
}
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;
} catch (Exception e) {
return false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
@@ -160,6 +155,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
LimeLog.info("Using hardware decoding");
return true;
}
private void startRendererThread()
@@ -167,60 +164,53 @@ 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 boolean start(VideoDepacketizer depacketizer) {
this.depacketizer = depacketizer;
startRendererThread();
return true;
}
@Override
@@ -239,24 +229,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)
{
@@ -264,8 +237,18 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
// Clear old input data
buf.clear();
if (needsSpsBitstreamFixup || needsSpsNumRefFixup) {
int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags();
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0 &&
(needsSpsBitstreamFixup || needsSpsNumRefFixup)) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
if (header.data[header.offset+4] == 0x67) {
// TI OMAP4 requires a reference frame count of 1 to decode successfully
@@ -312,7 +295,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoder.queueInputBuffer(inputIndex,
0, spsLength,
0, mcFlags);
0, codecFlags);
return true;
}
}
@@ -325,7 +308,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoder.queueInputBuffer(inputIndex,
0, decodeUnit.getDataLength(),
0, mcFlags);
0, codecFlags);
}
return true;
@@ -333,7 +316,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
@Override
public int getCapabilities() {
return fastInputQueueing ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return 0;
}
/**
@@ -1,36 +0,0 @@
package com.limelight.binding.video;
import android.content.Context;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.Type;
import android.view.Surface;
public class RsRenderer {
private RenderScript rs;
private Allocation renderBuffer;
public RsRenderer(Context context, int width, int height, Surface renderTarget) {
rs = RenderScript.create(context);
Type.Builder tb = new Type.Builder(rs, Element.RGBA_8888(rs));
tb.setX(width);
tb.setY(height);
Type bufferType = tb.create();
renderBuffer = Allocation.createTyped(rs, bufferType, Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
renderBuffer.setSurface(renderTarget);
}
public void release() {
renderBuffer.setSurface(null);
renderBuffer.destroy();
rs.destroy();
}
public void render(byte[] rgbData) {
renderBuffer.copyFrom(rgbData);
renderBuffer.ioSend();
}
}
+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();
}
}
}