Compare commits

..

45 Commits

Author SHA1 Message Date
Cameron Gutman cf80a7380c Increment version to 2.1 2014-03-26 00:14:24 -04:00
Cameron Gutman ceb957e7f3 Add a toast if the IP field isn't filled 2014-03-26 00:13:46 -04:00
Cameron Gutman 669691d8e8 Remove the annoying focus highlight on the streaming button. Hide the keyboard on the connect screen. 2014-03-26 00:13:23 -04:00
Cameron Gutman ea57e48ed5 Add keyboard support 2014-03-25 23:59:48 -04:00
Cameron Gutman f744c7d9c4 Make the cursor on the target track more true to the client input by scaling based on screen resolution 2014-03-25 20:43:45 -04:00
Cameron Gutman 0d42beca93 Add support for secondary and tertiary mouse buttons 2014-03-25 20:16:23 -04:00
Cameron Gutman bafb9e6230 Make streaming button focusable so the keyboard doesn't appear when starting limelight 2014-03-21 21:36:07 -04:00
Cameron Gutman 5d30a3f4ab Increment version code for redeployment 2014-03-21 21:23:02 -04:00
Cameron Gutman 70a1dd56b4 A bit more cleanup 2014-03-21 21:16:53 -04:00
Cameron Gutman 9857017b22 Dump decoders before starting for logging purposes. Remove the software decoders from the blacklist since they are properly detected by the high profile check now. 2014-03-21 20:54:33 -04:00
Cameron Gutman 53474c7d28 Check that the decoder supports high profile H.264 before using it 2014-03-21 20:29:22 -04:00
Cameron Gutman 283c1060a3 Increment version 2014-03-21 19:32:42 -04:00
Cameron Gutman eb7c54b95c Fix some handling of the Android activity lifecycle that upset the window manager 2014-03-21 19:32:24 -04:00
Cameron Gutman 735ad02fb4 The surface format must be RGBX if we're doing CPU rendering. If the surface is RGB565, the renderer will not properly format the output and the decoder will crash. 2014-03-21 18:20:55 -04:00
Cameron Gutman c1ad6f83b9 Remove touchscreen requirement 2014-03-17 20:55:37 -04:00
Cameron Gutman 209b649eb8 Increment version 2014-03-17 14:21:16 -04:00
Cameron Gutman 9bf7bd366f Update common 2014-03-17 14:21:02 -04:00
Cameron Gutman cfd788bec0 Use the minimum stream buffer size and the sample size when choosing a buffer size 2014-03-17 14:20:12 -04:00
Cameron Gutman 5284db761c Fix leftover warning 2014-03-17 12:39:17 -04:00
Cameron Gutman 07b5e9224c Don't explicitly specify a pixel format for the rendering surface. The system will choose the most optimal format if we leave it at the default. 2014-03-17 12:34:47 -04:00
Cameron Gutman f31736ee2d Set the volume buttons to control the music stream while streaming 2014-03-17 03:55:51 -04:00
Cameron Gutman 47a3a959e5 Update common 2014-03-16 18:28:51 -04:00
Cameron Gutman c2a679e5ef File mode modified on libs 2014-03-16 18:23:03 -04:00
Cameron Gutman 71b4980553 Rebuild JNI Opus decoder 2014-03-16 17:24:43 -04:00
Cameron Gutman 0b360f8f1d Calculate a proper value for the sample buffer (48000 Hz * 2 bytes per sample * 2.5 ms) 2014-03-16 17:16:51 -04:00
Cameron Gutman 52823df62f Don't use large heap anymore for improved performance 2014-03-15 13:17:22 -04:00
Cameron Gutman 39472bdc7b Update common for reduced memory usage 2014-03-14 22:26:25 -04:00
Cameron Gutman 49ae5a6d0c Add icons by XDA member MalaBouM 2014-03-14 22:15:26 -04:00
Cameron Gutman 1c426417f3 Increment version 2014-03-13 01:38:48 -04:00
Cameron Gutman 2cd41b5141 Update common 2014-03-13 01:38:29 -04:00
Cameron Gutman 74d9afd685 Update common 2014-02-27 17:19:26 -05:00
Cameron Gutman e13241eebf Use LimeLog for logging 2014-02-26 17:20:10 -05:00
Cameron Gutman 700ec361b0 Update common 2014-02-26 17:19:49 -05:00
Cameron Gutman c7f26a179d Add the frame skipping code back for certain decoders (Qualcomm) that are slow when rendering all frames and stall the whole decoder pipeline 2014-02-26 01:56:05 -05:00
Cameron Gutman 4b2552ed64 Set MediaCodec flags on the input buffer based on the DU flags 2014-02-26 01:01:54 -05:00
Cameron Gutman ddc70e39c9 Only use direct submit on decoders that have (effectively) infinite input buffers 2014-02-19 22:18:26 -05:00
Cameron Gutman 61c0de6ca1 Update common jar 2014-02-19 21:01:09 -05:00
Cameron Gutman b0c4d47962 Report direct submit support for the MediaCodec decoder renderer 2014-02-19 21:00:39 -05:00
Cameron Gutman 6fc848ef56 Block for 100 ms instead of 100 us waiting for a frame. This reduces the CPU time wasted by useless iterations of the rendering loop. 2014-02-19 19:49:57 -05:00
Cameron Gutman c537af2273 Render the latest frame available at the time 2014-02-19 19:40:35 -05:00
Cameron Gutman 839540dc74 UI updates to make options more clear. Add scroll view to UI. 2014-02-19 19:22:54 -05:00
Cameron Gutman 22ecdd9cf8 Don't skip frames when rendering 2014-02-19 19:22:00 -05:00
Cameron Gutman bf1c7dd675 Update common jar 2014-02-17 19:22:23 -05:00
Cameron Gutman 426b40ae82 Don't allocate a new BufferInfo object for each output buffer 2014-02-17 14:21:42 -05:00
Michelle Bergeron 6683c25a39 Update readme to match Limelight-PC's changes 2014-01-21 16:54:32 -05:00
20 changed files with 751 additions and 309 deletions
+4 -4
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="1"
android:versionName="1.0" >
android:versionCode="6"
android:versionName="2.1" >
<uses-sdk
android:minSdkVersion="16"
@@ -10,9 +10,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
android:largeHeap="true"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
@@ -22,7 +23,6 @@
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+8 -8
View File
@@ -3,22 +3,21 @@
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software, and created a version that can be run on any Android device.
Limelight will allow you to stream your full collection of Steam games from your
Windows PC to your Android device on the same network.
Limelight will allow you to stream your full collection of Steam games from your Windows PC to your Android device on the same network.
Streaming can be done remotely using the [Shield Proxy](http://forum.xda-developers.com/showthread.php?t=2435481)
application.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OSX and Linux.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows Phone](https://github.com/limelight-stream/limelight-wp) are also in development.
##Features
* Streams Steam and all of your games from your PC to your Android device
* Full gamepad support for MOGA, XBOX 360, PS3, OUYA, and Shield
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
##Features in development
* Use mDNS to scan for compatible GFE machines on the network
* Use mDNS to scan for compatible GeForce Experience (GFE) machines on the network
* Choose from the list of available games instead of just launching Steam
* Keyboard input
@@ -37,8 +36,7 @@ application.
##Usage
* Ensure your Android device and your PC are on the same network or you're
running [Shield Proxy](http://forum.xda-developers.com/showthread.php?t=2435481).
* Ensure your Android device and your PC are on the same network or you're running [Shield Proxy](http://forum.xda-developers.com/showthread.php?t=2435481).
* Turn on Shield Streaming in the GFE settings
* In Limelight, enter your PC's IP or Hostname and click "Pair".
* Accept the pairing confirmation on your PC
@@ -47,12 +45,14 @@ application.
##Contribute
This project is being actively developed at [XDA](http://forum.xda-developers.com/showthread.php?t=2505510)
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
1. Fork us
2. Write code
3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors
* [Cameron Gutman](https://github.com/cgutman)
+6 -8
View File
@@ -36,19 +36,17 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
}
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 hostTextView=0x7f080000;
public static final int imageQualityCheckbox=0x7f08000a;
public static final int pairButton=0x7f080002;
public static final int res1080pSelected=0x7f08000c;
public static final int res720pSelected=0x7f08000b;
public static final int resolutionGroup=0x7f080007;
public static final int rr30Selected=0x7f080008;
public static final int rr60Selected=0x7f080009;
public static final int rrGroup=0x7f080003;
public static final int softwareDec=0x7f080004;
public static final int statusButton=0x7f080001;
public static final int surfaceView=0x7f08000d;
public static final int streamConfigGroup=0x7f080003;
public static final int surfaceView=0x7f08000b;
}
public static final class layout {
public static final int activity_connection=0x7f030000;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+2 -2
View File
@@ -28,9 +28,9 @@ int nv_opus_get_channel_count(void) {
return 2;
}
// This number assumes 2 channels at 48 KHz
// This number assumes 16-bit samples at 48 KHz with 2.5 ms frames
int nv_opus_get_max_out_shorts(void) {
return 512*nv_opus_get_channel_count();
return 240*nv_opus_get_channel_count();
}
// The Opus stream is 48 KHz
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 64 KiB

+46 -54
View File
@@ -1,4 +1,4 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
@@ -7,6 +7,10 @@
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".Connection" >
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<EditText
android:id="@+id/hostTextView"
@@ -30,7 +34,7 @@
<requestFocus />
</Button>
</Button>
<Button
android:id="@+id/pairButton"
@@ -43,8 +47,9 @@
<RadioGroup
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/rrGroup"
android:layout_below="@+id/rrGroup"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/streamConfigGroup"
android:layout_marginTop="25dp"
android:orientation="vertical" >
@@ -58,7 +63,7 @@
android:id="@+id/autoDec"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Auto-select Decoder" />
android:text="Auto-select Decoder (Recommended)" />
<RadioButton
android:id="@+id/hardwareDec"
@@ -68,56 +73,43 @@
</RadioGroup>
<RadioGroup
android:id="@+id/rrGroup"
android:id="@+id/streamConfigGroup"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/resolutionGroup"
android:layout_below="@+id/resolutionGroup"
android:layout_marginTop="15dp"
android:orientation="horizontal" >
<RadioButton
android:id="@+id/rr30Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="30 FPS" />
<RadioButton
android:id="@+id/rr60Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="60 FPS (may increase lag)" />
</RadioGroup>
<RadioGroup
android:id="@+id/resolutionGroup"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/imageQualityCheckbox"
android:layout_below="@+id/imageQualityCheckbox"
android:layout_marginTop="17dp"
android:orientation="horizontal" >
<RadioButton
android:id="@+id/res720pSelected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="720p" />
<RadioButton
android:id="@+id/res1080pSelected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1080p (may increase lag)" />
</RadioGroup>
<CheckBox
android:id="@+id/imageQualityCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/pairButton"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:text="Prefer image quality over performance" />
android:layout_marginTop="25dp"
android:orientation="vertical" >
</RelativeLayout>
<RadioButton
android:id="@+id/config720p30Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="720p 30 FPS (Only recommended for poor devices or networks)" />
<RadioButton
android:id="@+id/config720p60Selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
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: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:text="1080p 60 FPS (Requires extremely fast device and network)" />
</RadioGroup>
</RelativeLayout>
</ScrollView>
+55 -47
View File
@@ -15,8 +15,8 @@ import com.limelight.nvstream.http.NvHTTP;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
@@ -31,8 +31,7 @@ public class Connection extends Activity {
private Button statusButton, pairButton;
private TextView hostText;
private SharedPreferences prefs;
private CheckBox qualityCheckbox;
private RadioButton rbutton720p, rbutton1080p, rbutton30fps, rbutton60fps;
private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
private RadioButton forceSoftDec, autoDec, forceHardDec;
private static final String DEFAULT_HOST = "";
@@ -54,38 +53,45 @@ public class Connection extends Activity {
setContentView(R.layout.activity_connection);
// Hide the keyboard by default
this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
this.statusButton = (Button) findViewById(R.id.statusButton);
this.pairButton = (Button) findViewById(R.id.pairButton);
this.hostText = (TextView) findViewById(R.id.hostTextView);
this.qualityCheckbox = (CheckBox) findViewById(R.id.imageQualityCheckbox);
this.rbutton720p = (RadioButton) findViewById(R.id.res720pSelected);
this.rbutton1080p = (RadioButton) findViewById(R.id.res1080pSelected);
this.rbutton30fps = (RadioButton) findViewById(R.id.rr30Selected);
this.rbutton60fps = (RadioButton) findViewById(R.id.rr60Selected);
this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected);
this.rbutton720p60 = (RadioButton) findViewById(R.id.config720p60Selected);
this.rbutton1080p30 = (RadioButton) findViewById(R.id.config1080p30Selected);
this.rbutton1080p60 = (RadioButton) findViewById(R.id.config1080p60Selected);
this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
this.autoDec = (RadioButton) findViewById(R.id.autoDec);
this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST));
this.qualityCheckbox.setChecked(prefs.getBoolean(Game.QUALITY_PREF_STRING, false));
if (prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720) {
rbutton720p.setChecked(true);
rbutton1080p.setChecked(false);
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;
rbutton720p30.setChecked(false);
rbutton720p60.setChecked(false);
rbutton1080p30.setChecked(false);
rbutton1080p60.setChecked(false);
if (res720p) {
if (fps30) {
rbutton720p30.setChecked(true);
}
else {
rbutton720p60.setChecked(true);
}
}
else {
rbutton1080p.setChecked(true);
rbutton720p.setChecked(false);
}
if (prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30) {
rbutton30fps.setChecked(true);
rbutton60fps.setChecked(false);
}
else {
rbutton60fps.setChecked(true);
rbutton30fps.setChecked(false);
if (fps30) {
rbutton1080p30.setChecked(true);
}
else {
rbutton1080p60.setChecked(true);
}
}
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
@@ -115,19 +121,25 @@ public class Connection extends Activity {
return;
}
if (buttonView == rbutton30fps) {
prefs.edit().putInt(Game.REFRESH_RATE_PREF_STRING, 30).commit();
}
else if (buttonView == rbutton60fps) {
prefs.edit().putInt(Game.REFRESH_RATE_PREF_STRING, 60).commit();
}
else if (buttonView == rbutton720p) {
if (buttonView == rbutton720p30) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
putInt(Game.HEIGHT_PREF_STRING, 720).commit();
putInt(Game.HEIGHT_PREF_STRING, 720).
putInt(Game.REFRESH_RATE_PREF_STRING, 30).commit();
}
else if (buttonView == rbutton1080p) {
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();
}
else if (buttonView == rbutton1080p30) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
putInt(Game.HEIGHT_PREF_STRING, 1080).commit();
putInt(Game.HEIGHT_PREF_STRING, 1080).
putInt(Game.REFRESH_RATE_PREF_STRING, 30).commit();
}
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();
}
else if (buttonView == forceSoftDec) {
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
@@ -140,26 +152,22 @@ public class Connection extends Activity {
}
}
};
rbutton720p.setOnCheckedChangeListener(occl);
rbutton1080p.setOnCheckedChangeListener(occl);
rbutton30fps.setOnCheckedChangeListener(occl);
rbutton60fps.setOnCheckedChangeListener(occl);
rbutton720p30.setOnCheckedChangeListener(occl);
rbutton720p60.setOnCheckedChangeListener(occl);
rbutton1080p30.setOnCheckedChangeListener(occl);
rbutton1080p60.setOnCheckedChangeListener(occl);
forceSoftDec.setOnCheckedChangeListener(occl);
forceHardDec.setOnCheckedChangeListener(occl);
autoDec.setOnCheckedChangeListener(occl);
this.qualityCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(Game.QUALITY_PREF_STRING, isChecked);
editor.commit();
}
});
this.statusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
if (Connection.this.hostText.getText().length() == 0) {
Toast.makeText(Connection.this, "Please enter the target PC's IP address in the text box at the top of the screen.", Toast.LENGTH_LONG).show();
return;
}
Intent intent = new Intent(Connection.this, Game.class);
intent.putExtra("host", Connection.this.hostText.getText().toString());
Connection.this.startActivity(intent);
@@ -182,7 +190,7 @@ public class Connection extends Activity {
}
if (macAddress == null) {
System.out.println("Couldn't find a MAC address");
LimeLog.severe("Couldn't find a MAC address");
return;
}
+234 -146
View File
@@ -7,15 +7,19 @@ import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -39,25 +43,31 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
private short leftStickY = 0x0000;
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
private int lastButtonState = 0;
private int lastTouchX = 0;
private int lastTouchY = 0;
private boolean hasMoved = false;
private KeyboardTranslator keybTranslator;
private int height;
private int width;
private Point screenSize = new Point(0, 0);
private NvConnection conn;
private SpinnerDialog spinner;
private boolean displayedFailureDialog = false;
public static final String PREFS_FILE_NAME = "gameprefs";
public static final String QUALITY_PREF_STRING = "Quality";
public static final String WIDTH_PREF_STRING = "Width";
public static final String HEIGHT_PREF_STRING = "Height";
public static final String REFRESH_RATE_PREF_STRING = "RefreshRate";
public static final String WIDTH_PREF_STRING = "ResH";
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 int DEFAULT_WIDTH = 1280;
public static final int DEFAULT_HEIGHT = 720;
public static final int DEFAULT_REFRESH_RATE = 30;
public static final int DEFAULT_REFRESH_RATE = 60;
public static final int DEFAULT_DECODER = 0;
public static final int FORCE_HARDWARE_DECODER = -1;
@@ -78,6 +88,9 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Change volume button behavior
setVolumeControlStream(AudioManager.STREAM_MUSIC);
// Inflate the content
setContentView(R.layout.activity_game);
@@ -87,7 +100,6 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
sv.setOnTouchListener(this);
SurfaceHolder sh = sv.getHolder();
sh.setFormat(PixelFormat.RGBX_8888);
// Start the spinner
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
@@ -95,9 +107,6 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
// Read the stream preferences
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
int drFlags = 0;
if (prefs.getBoolean(QUALITY_PREF_STRING, false)) {
drFlags |= VideoDecoderRenderer.FLAG_PREFER_QUALITY;
}
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
case Game.FORCE_SOFTWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
@@ -109,18 +118,22 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
break;
}
int width, height, refreshRate;
int refreshRate;
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);
sh.setFixedSize(width, height);
Display display = getWindowManager().getDefaultDisplay();
display.getSize(screenSize);
// Warn the user if they're on a metered connection
checkDataConnection();
// Start the connection
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
new StreamConfiguration(width, height, refreshRate));
keybTranslator = new KeyboardTranslator(conn);
conn.start(PlatformBinding.getDeviceName(), sv.getHolder(), drFlags,
PlatformBinding.getAudioRenderer(), new ConfigurableDecoderRenderer());
}
@@ -129,10 +142,11 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
{
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (mgr.isActiveNetworkMetered()) {
displayMessage("Warning: Your active network connection is metered!");
displayTransientMessage("Warning: Your active network connection is metered!");
}
}
@SuppressLint("InlinedApi")
private void hideSystemUi() {
runOnUiThread(new Runnable() {
@Override
@@ -157,142 +171,180 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
}
@Override
public void onPause() {
displayedFailureDialog = true;
conn.stop();
finish();
super.onPause();
}
@Override
protected void onDestroy() {
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs();
Dialog.closeDialogs();
super.onDestroy();
displayedFailureDialog = true;
conn.stop();
finish();
}
private static byte getModifierState(KeyEvent event) {
byte modifier = 0;
if (event.isShiftPressed()) {
modifier |= KeyboardPacket.MODIFIER_SHIFT;
}
if (event.isCtrlPressed()) {
modifier |= KeyboardPacket.MODIFIER_CTRL;
}
if (event.isAltPressed()) {
modifier |= KeyboardPacket.MODIFIER_ALT;
}
return modifier;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
default:
return super.onKeyDown(keyCode, event);
if (event.getDevice() != null &&
(event.getDevice().getSources() & InputDevice.SOURCE_KEYBOARD) != 0) {
short translated = keybTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyDown(keyCode, event);
}
keybTranslator.sendKeyDown(translated,
getModifierState(event));
}
// We detect back+start as the special button combo
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
{
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
else {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
default:
return super.onKeyDown(keyCode, event);
}
// We detect back+start as the special button combo
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
{
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
}
sendControllerInputPacket();
}
sendControllerInputPacket();
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
default:
return super.onKeyUp(keyCode, event);
if (event.getDevice() != null &&
(event.getDevice().getSources() & InputDevice.SOURCE_KEYBOARD) != 0) {
short translated = keybTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyUp(keyCode, event);
}
keybTranslator.sendKeyUp(translated,
getModifierState(event));
}
else {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
default:
return super.onKeyUp(keyCode, event);
}
// If one of the two is up, the special button comes up too
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
{
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
}
sendControllerInputPacket();
}
// If one of the two is up, the special button comes up too
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
{
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
}
sendControllerInputPacket();
return true;
}
@@ -365,17 +417,36 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
// This case is for mice
else if (event.getSource() == InputDevice.SOURCE_MOUSE)
{
switch (event.getActionMasked())
{
case MotionEvent.ACTION_DOWN:
conn.sendMouseButtonDown((byte) 0x01);
break;
case MotionEvent.ACTION_UP:
conn.sendMouseButtonUp((byte) 0x01);
break;
default:
return super.onTouchEvent(event);
int changedButtons = event.getButtonState() ^ lastButtonState;
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
conn.sendMouseButtonDown((byte) 0x01);
}
else {
conn.sendMouseButtonUp((byte) 0x01);
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
conn.sendMouseButtonDown((byte) 0x03);
}
else {
conn.sendMouseButtonUp((byte) 0x03);
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
conn.sendMouseButtonDown((byte) 0x02);
}
else {
conn.sendMouseButtonUp((byte) 0x02);
}
}
lastButtonState = event.getButtonState();
}
else
{
@@ -491,8 +562,15 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
lastMouseY != Integer.MIN_VALUE &&
!(lastMouseX == eventX && lastMouseY == eventY))
{
conn.sendMouseMove((short)(eventX - lastMouseX),
(short)(eventY - lastMouseY));
int deltaX = eventX - lastMouseX;
int deltaY = eventY - lastMouseY;
// Scale the deltas if the device resolution is different
// than the stream resolution
deltaX = (int)Math.round((double)deltaX * ((double)width / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * ((double)height / (double)screenSize.y));
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
// Update pointer location for delta calculation next time
@@ -567,4 +645,14 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
}
});
}
@Override
public void displayTransientMessage(final String message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
}
});
}
}
+260
View File
@@ -0,0 +1,260 @@
package com.limelight;
import android.view.KeyEvent;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.KeycodeTranslator;
/**
* Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg
* @author Cameron Gutman
*/
public class KeyboardTranslator extends KeycodeTranslator {
/**
* GFE's prefix for every key code
*/
public static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_PERIOD = 46;
public static final int VK_INSERT = 155;
public static final int VK_OPEN_BRACKET = 91;
public static final int VK_WINDOWS = 524;
public static final int VK_MINUS = 45;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
public static final int VK_PAGE_UP = 33;
public static final int VK_PAGE_DOWN = 34;
public static final int VK_PLUS = 521;
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SHIFT = 16;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
public static final int VK_TAB = 9;
public static final int VK_LEFT = 37;
public static final int VK_RIGHT = 39;
public static final int VK_UP = 38;
public static final int VK_DOWN = 40;
public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19;
/**
* Constructs a new translator for the specified connection
* @param conn the connection to which the translated codes are sent
*/
public KeyboardTranslator(NvConnection conn) {
super(conn);
}
/**
* Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated
* @return a GFE keycode for the given keycode
*/
@Override
public short translate(int keycode) {
int translated;
/* There seems to be no clean mapping between Android key codes
* and what Nvidia sends over the wire. If someone finds one,
* I'll happily delete this code :)
*/
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
}
else if (keycode >= KeyEvent.KEYCODE_A &&
keycode <= KeyEvent.KEYCODE_Z) {
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
}
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
}
else if (keycode >= KeyEvent.KEYCODE_F1 &&
keycode <= KeyEvent.KEYCODE_F12) {
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
}
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = VK_ALT;
break;
case KeyEvent.KEYCODE_BACKSLASH:
translated = 0xdc;
break;
case KeyEvent.KEYCODE_CAPS_LOCK:
translated = VK_CAPS_LOCK;
break;
case KeyEvent.KEYCODE_CLEAR:
translated = VK_CLEAR;
break;
case KeyEvent.KEYCODE_COMMA:
translated = 0xbc;
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = VK_CONTROL;
break;
case KeyEvent.KEYCODE_DEL:
translated = VK_BACK_SPACE;
break;
case KeyEvent.KEYCODE_ENTER:
translated = 0x0d;
break;
case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb;
break;
case KeyEvent.KEYCODE_ESCAPE:
translated = VK_ESCAPE;
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
// Nvidia maps period to delete
translated = VK_PERIOD;
break;
case KeyEvent.KEYCODE_INSERT:
translated = -1;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
case KeyEvent.KEYCODE_META_RIGHT:
translated = VK_WINDOWS;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
case KeyEvent.KEYCODE_MOVE_END:
translated = VK_END;
break;
case KeyEvent.KEYCODE_MOVE_HOME:
translated = VK_HOME;
break;
case KeyEvent.KEYCODE_NUM_LOCK:
translated = VK_NUM_LOCK;
break;
case KeyEvent.KEYCODE_PAGE_DOWN:
translated = VK_PAGE_DOWN;
break;
case KeyEvent.KEYCODE_PAGE_UP:
translated = VK_PAGE_UP;
break;
case KeyEvent.KEYCODE_PERIOD:
translated = 0xbe;
break;
case KeyEvent.KEYCODE_RIGHT_BRACKET:
translated = 0xdd;
break;
case KeyEvent.KEYCODE_SCROLL_LOCK:
translated = VK_SCROLL_LOCK;
break;
case KeyEvent.KEYCODE_SEMICOLON:
translated = 0xba;
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = VK_SHIFT;
break;
case KeyEvent.KEYCODE_SLASH:
translated = 0xbf;
break;
case KeyEvent.KEYCODE_SPACE:
translated = VK_SPACE;
break;
case KeyEvent.KEYCODE_SYSRQ:
// Android defines this as SysRq/PrntScrn
translated = VK_PRINTSCREEN;
break;
case KeyEvent.KEYCODE_TAB:
translated = VK_TAB;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
translated = VK_LEFT;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
translated = VK_RIGHT;
break;
case KeyEvent.KEYCODE_DPAD_UP:
translated = VK_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
translated = VK_DOWN;
break;
case KeyEvent.KEYCODE_GRAVE:
translated = VK_BACK_QUOTE;
break;
case KeyEvent.KEYCODE_APOSTROPHE:
translated = 0xde;
break;
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
default:
System.out.println("No key for "+keycode);
return 0;
}
}
return (short) ((KEY_PREFIX << 8) | translated);
}
}
@@ -4,15 +4,19 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override
public void streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
switch (channelCount)
{
@@ -26,11 +30,20 @@ public class AndroidAudioRenderer implements AudioRenderer {
throw new IllegalArgumentException("Decoder returned unhandled channel count");
}
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
LimeLog.info("Audio track buffer size: "+bufferSize);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
1024, // 1KB buffer
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
@@ -47,4 +60,9 @@ public class AndroidAudioRenderer implements AudioRenderer {
track.release();
}
}
@Override
public int getCapabilities() {
return 0;
}
}
@@ -6,8 +6,10 @@ import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import android.graphics.PixelFormat;
import android.view.SurfaceHolder;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
@@ -120,19 +122,22 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
// Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE;
System.out.println("Using high quality decoding");
LimeLog.info("Using high quality decoding");
}
SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err);
}
AvcDecoder.setRenderTarget(((SurfaceHolder)renderTarget).getSurface());
AvcDecoder.setRenderTarget(sh.getSurface());
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
System.out.println("Using software decoding (performance level: "+perfLevel+")");
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
}
@Override
@@ -207,4 +212,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
return (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
}
@Override
public int getCapabilities() {
return 0;
}
}
@@ -40,4 +40,9 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
return decoderRenderer.submitDecodeUnit(du);
}
@Override
public int getCapabilities() {
return decoderRenderer.getCapabilities();
}
}
@@ -4,12 +4,15 @@ import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo;
@@ -22,15 +25,17 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread;
private int redrawRate;
private boolean needsSpsFixup;
private boolean fastInputQueueing;
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupDecoderPrefixes;
public static final List<String> fastInputQueueingPrefixes;
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
blacklistedDecoderPrefixes.add("omx.google");
// TI's decoder technically supports high profile but doesn't work for some reason
blacklistedDecoderPrefixes.add("omx.TI");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
static {
@@ -38,8 +43,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
spsFixupDecoderPrefixes.add("omx.nvidia");
}
private static boolean decoderNeedsSpsFixup(String decoderName) {
for (String badPrefix : spsFixupDecoderPrefixes) {
static {
fastInputQueueingPrefixes = new LinkedList<String>();
fastInputQueueingPrefixes.add("omx.nvidia");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
if (prefix.equalsIgnoreCase(badPrefix)) {
@@ -50,38 +60,58 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
return false;
}
public static MediaCodecInfo findSafeDecoder() {
public static void dumpDecoders() {
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
boolean badCodec = false;
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
for (String badPrefix : blacklistedDecoderPrefixes) {
String name = codecInfo.getName();
if (name.length() >= badPrefix.length()) {
String prefix = name.substring(0, badPrefix.length());
if (prefix.equalsIgnoreCase(badPrefix)) {
badCodec = true;
break;
}
LimeLog.info("Decoder: "+codecInfo.getName());
for (String type : codecInfo.getSupportedTypes()) {
LimeLog.info("\t"+type);
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
for (CodecProfileLevel profile : caps.profileLevels) {
LimeLog.info("\t\t"+profile.profile+" "+profile.level);
}
}
if (badCodec) {
System.out.println("Blacklisted decoder: "+codecInfo.getName());
}
}
public static MediaCodecInfo findSafeDecoder() {
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264 high profile
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
System.out.println("Selected decoder: "+codecInfo.getName());
return codecInfo;
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
}
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}
@@ -93,17 +123,24 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.redrawRate = redrawRate;
dumpDecoders();
MediaCodecInfo safeDecoder = findSafeDecoder();
if (safeDecoder != null) {
videoDecoder = MediaCodec.createByCodecName(safeDecoder.getName());
needsSpsFixup = decoderNeedsSpsFixup(safeDecoder.getName());
needsSpsFixup = isDecoderInList(spsFixupDecoderPrefixes, safeDecoder.getName());
if (needsSpsFixup) {
System.out.println("Decoder "+safeDecoder.getName()+" needs SPS fixup");
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS fixup");
}
fastInputQueueing = isDecoderInList(fastInputQueueingPrefixes, safeDecoder.getName());
if (fastInputQueueing) {
LimeLog.info("Decoder "+safeDecoder.getName()+" supports fast input queueing");
}
}
else {
videoDecoder = MediaCodec.createDecoderByType("video/avc");
needsSpsFixup = false;
fastInputQueueing = false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
@@ -113,7 +150,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
System.out.println("Using hardware decoding");
LimeLog.info("Using hardware decoding");
}
private void startRendererThread()
@@ -122,30 +159,40 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
@Override
public void run() {
long nextFrameTimeUs = 0;
BufferInfo info = new BufferInfo();
while (!isInterrupted())
{
BufferInfo info = new BufferInfo();
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100);
// Block for a maximum of 100 ms
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100000);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
System.out.println("Output buffers changed");
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
System.out.println("Output format changed");
System.out.println("New output Format: " + videoDecoder.getOutputFormat());
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
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;
}
videoDecoder.releaseOutputBuffer(outIndex, render);
// Render that buffer if it's time for the next frame
videoDecoder.releaseOutputBuffer(lastIndex, render);
}
}
}
@@ -190,6 +237,17 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
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;
}
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
if (inputIndex >= 0)
{
@@ -209,7 +267,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
switch (header.length) {
case 26:
System.out.println("Modifying SPS (26)");
LimeLog.info("Modifying SPS (26)");
buf.put(header.data, header.offset, 24);
buf.put((byte) 0x11);
buf.put((byte) 0xe3);
@@ -218,7 +276,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
spsLength = header.length + 2;
break;
case 27:
System.out.println("Modifying SPS (27)");
LimeLog.info("Modifying SPS (27)");
buf.put(header.data, header.offset, 25);
buf.put((byte) 0x04);
buf.put((byte) 0x78);
@@ -227,7 +285,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
spsLength = header.length + 2;
break;
default:
System.out.println("Unknown SPS of length "+header.length);
LimeLog.warning("Unknown SPS of length "+header.length);
buf.put(header.data, header.offset, header.length);
spsLength = header.length;
break;
@@ -235,7 +293,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoder.queueInputBuffer(inputIndex,
0, spsLength,
0, decodeUnit.getFlags());
0, mcFlags);
return true;
}
}
@@ -248,9 +306,14 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
videoDecoder.queueInputBuffer(inputIndex,
0, decodeUnit.getDataLength(),
0, decodeUnit.getFlags());
0, mcFlags);
}
return true;
}
@Override
public int getCapabilities() {
return fastInputQueueing ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
}
}