Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37cf572c0c | |||
| 33c5254d6f | |||
| faa82ca9d6 | |||
| 0d35ea5207 | |||
| 579645c07c | |||
| 869cbe2e81 | |||
| 329a938bf8 | |||
| 411931cc27 | |||
| ce01223683 | |||
| e7501a488d | |||
| 5626e9663b | |||
| 01b35ccdd3 | |||
| e83bc747c8 | |||
| cbe4af7623 | |||
| fc9e45270a | |||
| 94c1fc2b66 | |||
| 49999634c1 | |||
| 09f4827d02 | |||
| 52e4e81e35 | |||
| 56b752f63f | |||
| 2e6e835a8e | |||
| d8b0a0ffb5 | |||
| b82d74474a | |||
| 508b855e36 | |||
| 5efcd606e3 | |||
| 3524cdd764 | |||
| 368cd8808d | |||
| b52a6ce93c | |||
| 7ab4e5d0a5 | |||
| 095dfd8035 | |||
| f7c33ef975 | |||
| 57b0bce5a4 | |||
| 7a017d7b97 | |||
| d2773be32e | |||
| 93a7d9f181 | |||
| 9703cf4ffe | |||
| 41ec64e87c | |||
| aca92a5056 | |||
| fc9c4d9aaa | |||
| d8c6a544f0 | |||
| 7e100f2c9c | |||
| 643b644e17 | |||
| 48a9d3ac12 | |||
| efdd1e2046 | |||
| 8a40892865 | |||
| 20635a3012 | |||
| b908c5cec3 | |||
| 1cb0b723f6 | |||
| 3f3c573c79 | |||
| e1253bbb59 | |||
| b2eb953f45 | |||
| 9638c15c93 | |||
| 8ce972ea7a | |||
| 968557d3a8 |
+5
-3
@@ -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="6"
|
||||
android:versionName="2.1" >
|
||||
android:versionCode="18"
|
||||
android:versionName="2.4" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
@@ -24,12 +25,13 @@
|
||||
<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
|
||||
android:name="com.limelight.Game"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:label="@string/title_activity_game"
|
||||
android:parentActivityName="com.limelight.Connection"
|
||||
android:theme="@style/FullscreenTheme" >
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
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.
|
||||
|
||||
Streaming can be done remotely using the [Shield Proxy](http://forum.xda-developers.com/showthread.php?t=2435481)
|
||||
application.
|
||||
Limelight will allow you to stream your full collection of Steam games from your Windows PC to your Android device,
|
||||
in your own home, or over the internet.
|
||||
|
||||
[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.
|
||||
|
||||
@@ -36,9 +34,10 @@ 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).
|
||||
* Turn on Shield Streaming in the GFE settings
|
||||
* In Limelight, enter your PC's IP or Hostname and click "Pair".
|
||||
* If you are connecting from outside the same network, turn on internet
|
||||
streaming
|
||||
* In Limelight, enter your PC's IP or Hostname and click "Pair"
|
||||
* Accept the pairing confirmation on your PC
|
||||
* In Limelight, click "Start Streaming"
|
||||
* Play games!
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"SupportedControllers" : {
|
||||
"Gamepad" : {},
|
||||
"Remote" : "false",
|
||||
"SecondScreen" : {
|
||||
"DPad" : "false",
|
||||
"AnalogSticks" : "0",
|
||||
"DigitalButtons" : "0",
|
||||
"Mouse" : "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-10
@@ -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.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -177,7 +247,12 @@ public class Connection extends Activity {
|
||||
this.pairButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View arg0) {
|
||||
Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_LONG).show();
|
||||
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;
|
||||
}
|
||||
|
||||
Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_SHORT).show();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -198,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
|
||||
@@ -235,4 +323,7 @@ public class Connection extends Activity {
|
||||
|
||||
}
|
||||
|
||||
private void updateBitrateLabel() {
|
||||
bitrateLabel.setText("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
|
||||
}
|
||||
}
|
||||
|
||||
+226
-224
@@ -1,12 +1,20 @@
|
||||
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;
|
||||
import com.limelight.binding.video.ConfigurableDecoderRenderer;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
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;
|
||||
@@ -18,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;
|
||||
@@ -33,14 +44,7 @@ import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
||||
public class Game extends Activity implements OnGenericMotionListener, OnTouchListener, NvConnectionListener {
|
||||
private short inputMap = 0x0000;
|
||||
private byte leftTrigger = 0x00;
|
||||
private byte rightTrigger = 0x00;
|
||||
private short rightStickX = 0x0000;
|
||||
private short rightStickY = 0x0000;
|
||||
private short leftStickX = 0x0000;
|
||||
private short leftStickY = 0x0000;
|
||||
public class Game extends Activity implements SurfaceHolder.Callback, OnGenericMotionListener, OnTouchListener, NvConnectionListener {
|
||||
private int lastMouseX = Integer.MIN_VALUE;
|
||||
private int lastMouseY = Integer.MIN_VALUE;
|
||||
private int lastButtonState = 0;
|
||||
@@ -48,6 +52,7 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
private int lastTouchY = 0;
|
||||
private boolean hasMoved = false;
|
||||
|
||||
private ControllerHandler controllerHandler;
|
||||
private KeyboardTranslator keybTranslator;
|
||||
|
||||
private int height;
|
||||
@@ -57,6 +62,12 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
private NvConnection conn;
|
||||
private SpinnerDialog spinner;
|
||||
private boolean displayedFailureDialog = false;
|
||||
private boolean connecting = false;
|
||||
private boolean connected = false;
|
||||
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
|
||||
private int drFlags = 0;
|
||||
|
||||
public static final String PREFS_FILE_NAME = "gameprefs";
|
||||
|
||||
@@ -64,11 +75,25 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
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;
|
||||
@@ -78,15 +103,24 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
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);
|
||||
@@ -106,7 +140,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;
|
||||
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
|
||||
case Game.FORCE_SOFTWARE_DECODER:
|
||||
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
|
||||
@@ -118,10 +151,11 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
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();
|
||||
@@ -129,13 +163,123 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
|
||||
// Warn the user if they're on a metered connection
|
||||
checkDataConnection();
|
||||
|
||||
// Make sure Wi-Fi is fully powered up
|
||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
||||
wifiLock.setReferenceCounted(false);
|
||||
wifiLock.acquire();
|
||||
|
||||
String host = Game.this.getIntent().getStringExtra("host");
|
||||
InetAddress addr;
|
||||
boolean enableLargePackets;
|
||||
try {
|
||||
// If this does a DNS lookup, it could cause a NetworkOnMainThread exception
|
||||
// Chances are if it has to do this, we're not on the same network anyways
|
||||
addr = InetAddress.getByName(host);
|
||||
|
||||
// Check if we can enable large packets if we get this far
|
||||
enableLargePackets = shouldEnableLargePackets(addr);
|
||||
} catch (Exception e) {
|
||||
// We don't want to deal with any exceptions here. The user will be notified
|
||||
// when the connection fails
|
||||
enableLargePackets = false;
|
||||
}
|
||||
|
||||
LimeLog.info("Using large packets? "+enableLargePackets);
|
||||
|
||||
// Start the connection
|
||||
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this,
|
||||
new StreamConfiguration(width, height, refreshRate));
|
||||
conn = new NvConnection(host, Game.this,
|
||||
new StreamConfiguration(width, height, refreshRate, bitrate * 1000,
|
||||
enableLargePackets ? 1460 : 1024), PlatformBinding.getCryptoProvider(this));
|
||||
keybTranslator = new KeyboardTranslator(conn);
|
||||
conn.start(PlatformBinding.getDeviceName(), sv.getHolder(), drFlags,
|
||||
PlatformBinding.getAudioRenderer(), new ConfigurableDecoderRenderer());
|
||||
controllerHandler = new ControllerHandler(conn);
|
||||
|
||||
// The connection will be started when the surface gets created
|
||||
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()
|
||||
@@ -147,8 +291,7 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
}
|
||||
|
||||
@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 OnGenericMotionListener, OnTouchLi
|
||||
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 OnGenericMotionListener, OnTouchLi
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
wifiLock.release();
|
||||
}
|
||||
|
||||
private static byte getModifierState(KeyEvent event) {
|
||||
byte modifier = 0;
|
||||
if (event.isShiftPressed()) {
|
||||
@@ -200,7 +357,7 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (event.getDevice() != null &&
|
||||
(event.getDevice().getSources() & InputDevice.SOURCE_KEYBOARD) != 0) {
|
||||
(event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) {
|
||||
short translated = keybTranslator.translate(event.getKeyCode());
|
||||
if (translated == 0) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
@@ -210,64 +367,9 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
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:
|
||||
if (!controllerHandler.handleButtonDown(keyCode, event)) {
|
||||
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();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -275,8 +377,18 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
|
||||
@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().getSources() & InputDevice.SOURCE_KEYBOARD) != 0) {
|
||||
(event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) {
|
||||
short translated = keybTranslator.translate(event.getKeyCode());
|
||||
if (translated == 0) {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
@@ -286,63 +398,9 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
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:
|
||||
if (!controllerHandler.handleButtonUp(keyCode, event)) {
|
||||
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();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -446,6 +504,8 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
}
|
||||
}
|
||||
|
||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||
|
||||
lastButtonState = event.getButtonState();
|
||||
}
|
||||
else
|
||||
@@ -458,92 +518,13 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
InputDevice dev = event.getDevice();
|
||||
|
||||
if (dev == null) {
|
||||
System.err.println("Unknown device");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
float LS_X = event.getAxisValue(MotionEvent.AXIS_X);
|
||||
float LS_Y = event.getAxisValue(MotionEvent.AXIS_Y);
|
||||
|
||||
float RS_X, RS_Y, L2, R2;
|
||||
|
||||
InputDevice.MotionRange leftTriggerRange = dev.getMotionRange(MotionEvent.AXIS_LTRIGGER);
|
||||
InputDevice.MotionRange rightTriggerRange = dev.getMotionRange(MotionEvent.AXIS_RTRIGGER);
|
||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||
{
|
||||
// Ouya controller
|
||||
L2 = event.getAxisValue(MotionEvent.AXIS_LTRIGGER);
|
||||
R2 = event.getAxisValue(MotionEvent.AXIS_RTRIGGER);
|
||||
RS_X = event.getAxisValue(MotionEvent.AXIS_Z);
|
||||
RS_Y = event.getAxisValue(MotionEvent.AXIS_RZ);
|
||||
}
|
||||
else
|
||||
{
|
||||
InputDevice.MotionRange brakeRange = dev.getMotionRange(MotionEvent.AXIS_BRAKE);
|
||||
InputDevice.MotionRange gasRange = dev.getMotionRange(MotionEvent.AXIS_GAS);
|
||||
if (brakeRange != null && gasRange != null)
|
||||
{
|
||||
// Moga controller
|
||||
RS_X = event.getAxisValue(MotionEvent.AXIS_Z);
|
||||
RS_Y = event.getAxisValue(MotionEvent.AXIS_RZ);
|
||||
L2 = event.getAxisValue(MotionEvent.AXIS_BRAKE);
|
||||
R2 = event.getAxisValue(MotionEvent.AXIS_GAS);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Xbox controller
|
||||
RS_X = event.getAxisValue(MotionEvent.AXIS_RX);
|
||||
RS_Y = event.getAxisValue(MotionEvent.AXIS_RY);
|
||||
L2 = (event.getAxisValue(MotionEvent.AXIS_Z) + 1) / 2;
|
||||
R2 = (event.getAxisValue(MotionEvent.AXIS_RZ) + 1) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
InputDevice.MotionRange hatXRange = dev.getMotionRange(MotionEvent.AXIS_HAT_X);
|
||||
InputDevice.MotionRange hatYRange = dev.getMotionRange(MotionEvent.AXIS_HAT_Y);
|
||||
if (hatXRange != null && hatYRange != null)
|
||||
{
|
||||
// Xbox controller D-pad
|
||||
float hatX, hatY;
|
||||
|
||||
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||
|
||||
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||
if (hatX < -0.5) {
|
||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
}
|
||||
if (hatX > 0.5) {
|
||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
}
|
||||
if (hatY < -0.5) {
|
||||
inputMap |= ControllerPacket.UP_FLAG;
|
||||
}
|
||||
if (hatY > 0.5) {
|
||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
leftStickX = (short)Math.round(LS_X * 0x7FFF);
|
||||
leftStickY = (short)Math.round(-LS_Y * 0x7FFF);
|
||||
|
||||
rightStickX = (short)Math.round(RS_X * 0x7FFF);
|
||||
rightStickY = (short)Math.round(-RS_Y * 0x7FFF);
|
||||
|
||||
leftTrigger = (byte)Math.round(L2 * 0xFF);
|
||||
rightTrigger = (byte)Math.round(R2 * 0xFF);
|
||||
|
||||
sendControllerInputPacket();
|
||||
return true;
|
||||
if (controllerHandler.handleMotionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||
{
|
||||
@@ -577,11 +558,6 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
lastMouseX = eventX;
|
||||
lastMouseY = eventY;
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket() {
|
||||
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
@@ -615,6 +591,7 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
displayedFailureDialog = true;
|
||||
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
||||
conn.stop();
|
||||
connecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +602,7 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
e.printStackTrace();
|
||||
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
||||
conn.stop();
|
||||
connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,6 +611,9 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
spinner.dismiss();
|
||||
spinner = null;
|
||||
|
||||
connecting = false;
|
||||
connected = true;
|
||||
|
||||
hideSystemUi();
|
||||
}
|
||||
|
||||
@@ -655,4 +636,25 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
if (!connected && !connecting) {
|
||||
connecting = true;
|
||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||
PlatformBinding.getAudioRenderer(), new ConfigurableDecoderRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
if (connected) {
|
||||
conn.stop();
|
||||
connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.limelight.binding;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
public class PlatformBinding {
|
||||
public static String getDeviceName() {
|
||||
@@ -13,4 +17,8 @@ public class PlatformBinding {
|
||||
public static AudioRenderer getAudioRenderer() {
|
||||
return new AndroidAudioRenderer();
|
||||
}
|
||||
|
||||
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||
return new AndroidCryptoProvider(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.limelight.binding.crypto;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMWriter;
|
||||
import org.bouncycastle.x509.X509V3CertificateGenerator;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
private File certFile;
|
||||
private File keyFile;
|
||||
|
||||
private X509Certificate cert;
|
||||
private RSAPrivateKey key;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
static {
|
||||
// Install the Bouncy Castle provider
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
public AndroidCryptoProvider(Context c) {
|
||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||
|
||||
certFile = new File(dataPath + File.separator + "client.crt");
|
||||
keyFile = new File(dataPath + File.separator + "client.key");
|
||||
}
|
||||
|
||||
private byte[] loadFileToBytes(File f) {
|
||||
if (!f.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(f);
|
||||
byte[] fileData = new byte[(int) f.length()];
|
||||
fin.read(fileData);
|
||||
fin.close();
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadCertKeyPair() {
|
||||
byte[] certBytes = loadFileToBytes(certFile);
|
||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||
|
||||
// If either file was missing, we definitely can't succeed
|
||||
if (certBytes == null || keyBytes == null) {
|
||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
pemCertBytes = certBytes;
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
} catch (CertificateException e) {
|
||||
// May happen if the cert is corrupt
|
||||
LimeLog.warning("Corrupted certificate");
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
// May happen if the key is corrupt
|
||||
LimeLog.warning("Corrupted key");
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Loaded key pair from disk");
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("TrulyRandom")
|
||||
private boolean generateCertKeyPair() {
|
||||
X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
|
||||
X500Principal principalName = new X500Principal("CN=NVIDIA GameStream Client");
|
||||
|
||||
byte[] snBytes = new byte[8];
|
||||
new SecureRandom().nextBytes(snBytes);
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e1) {
|
||||
// Should never happen
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
Date expirationDate = new Date();
|
||||
|
||||
// Expires in 20 years
|
||||
expirationDate.setYear(expirationDate.getYear() + 20);
|
||||
|
||||
certGenerator.setSerialNumber(new BigInteger(snBytes).abs());
|
||||
certGenerator.setIssuerDN(principalName);
|
||||
certGenerator.setNotBefore(now);
|
||||
certGenerator.setNotAfter(expirationDate);
|
||||
certGenerator.setSubjectDN(principalName);
|
||||
certGenerator.setPublicKey(keyPair.getPublic());
|
||||
certGenerator.setSignatureAlgorithm("SHA1withRSA");
|
||||
|
||||
try {
|
||||
cert = certGenerator.generate(keyPair.getPrivate(), "BC");
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
// Save the resulting pair
|
||||
saveCertKeyPair();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveCertKeyPair() {
|
||||
try {
|
||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||
|
||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||
StringWriter strWriter = new StringWriter();
|
||||
PEMWriter pemWriter = new PEMWriter(strWriter);
|
||||
pemWriter.writeObject(cert);
|
||||
pemWriter.close();
|
||||
|
||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||
String pemStr = strWriter.getBuffer().toString();
|
||||
for (int i = 0; i < pemStr.length(); i++) {
|
||||
char c = pemStr.charAt(i);
|
||||
if (c != '\r')
|
||||
certWriter.append(c);
|
||||
}
|
||||
certWriter.close();
|
||||
|
||||
// Write the private out in PKCS8 format
|
||||
keyOut.write(key.getEncoded());
|
||||
|
||||
certOut.close();
|
||||
keyOut.close();
|
||||
|
||||
LimeLog.info("Saved generated key pair to disk");
|
||||
} catch (IOException e) {
|
||||
// This isn't good because it means we'll have
|
||||
// to re-pair next time
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public X509Certificate getClientCertificate() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (this) {
|
||||
// Return a loaded cert if we have one
|
||||
if (cert != null) {
|
||||
return cert;
|
||||
}
|
||||
|
||||
// No loaded cert yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return cert;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
|
||||
public RSAPrivateKey getClientPrivateKey() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (this) {
|
||||
// Return a loaded key if we have one
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// No loaded key yet, let's see if we have one on disk
|
||||
if (loadCertKeyPair()) {
|
||||
// Got one
|
||||
return key;
|
||||
}
|
||||
|
||||
// Try to generate a new key pair
|
||||
if (!generateCertKeyPair()) {
|
||||
// Failed
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load the generated pair
|
||||
loadCertKeyPair();
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getPemEncodedClientCertificate() {
|
||||
synchronized (this) {
|
||||
// Call our helper function to do the cert loading/generation for us
|
||||
getClientCertificate();
|
||||
|
||||
// Return a cached value if we have it
|
||||
return pemCertBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encodeBase64String(byte[] data) {
|
||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
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;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
public class ControllerHandler {
|
||||
private short inputMap = 0x0000;
|
||||
private byte leftTrigger = 0x00;
|
||||
private byte rightTrigger = 0x00;
|
||||
private short rightStickX = 0x0000;
|
||||
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>();
|
||||
|
||||
private NvConnection conn;
|
||||
|
||||
public ControllerHandler(NvConnection conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||
ControllerMapping mapping = new ControllerMapping();
|
||||
|
||||
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
|
||||
InputDevice.MotionRange leftTriggerRange = dev.getMotionRange(MotionEvent.AXIS_LTRIGGER);
|
||||
InputDevice.MotionRange rightTriggerRange = dev.getMotionRange(MotionEvent.AXIS_RTRIGGER);
|
||||
InputDevice.MotionRange brakeRange = dev.getMotionRange(MotionEvent.AXIS_BRAKE);
|
||||
InputDevice.MotionRange gasRange = dev.getMotionRange(MotionEvent.AXIS_GAS);
|
||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||
{
|
||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
|
||||
}
|
||||
else if (brakeRange != null && gasRange != null)
|
||||
{
|
||||
// Others use GAS and BRAKE (like Moga)
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
else
|
||||
{
|
||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
||||
if (rxRange != null && ryRange != null) {
|
||||
String devName = dev.getName();
|
||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||
// Xbox controllers use RX and RY for right stick
|
||||
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
|
||||
// Xbox controllers use Z and RZ for triggers
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
mapping.triggersIdleNegative = true;
|
||||
}
|
||||
else {
|
||||
// DS4 controller uses RX and RY for triggers
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
mapping.triggersIdleNegative = true;
|
||||
|
||||
mapping.isDualShock4 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||
InputDevice.MotionRange zRange = dev.getMotionRange(MotionEvent.AXIS_Z);
|
||||
InputDevice.MotionRange rzRange = dev.getMotionRange(MotionEvent.AXIS_RZ);
|
||||
|
||||
// Most other controllers use Z and RZ for the right stick
|
||||
if (zRange != null && rzRange != null) {
|
||||
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||
}
|
||||
else {
|
||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
||||
|
||||
// Try RX and RY now
|
||||
if (rxRange != null && ryRange != null) {
|
||||
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some devices have "hats" for d-pads
|
||||
InputDevice.MotionRange hatXRange = dev.getMotionRange(MotionEvent.AXIS_HAT_X);
|
||||
InputDevice.MotionRange hatYRange = dev.getMotionRange(MotionEvent.AXIS_HAT_Y);
|
||||
if (hatXRange != null && hatYRange != null) {
|
||||
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||
|
||||
mapping.hatXDeadzone = hatXRange.getFlat();
|
||||
mapping.hatYDeadzone = hatYRange.getFlat();
|
||||
}
|
||||
|
||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||
InputDevice.MotionRange lsXRange = dev.getMotionRange(mapping.leftStickXAxis);
|
||||
InputDevice.MotionRange lsYRange = dev.getMotionRange(mapping.leftStickYAxis);
|
||||
if (lsXRange != null) {
|
||||
mapping.leftStickXAxisDeadzone = lsXRange.getFlat();
|
||||
}
|
||||
if (lsYRange != null) {
|
||||
mapping.leftStickYAxisDeadzone = lsYRange.getFlat();
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||
InputDevice.MotionRange rsXRange = dev.getMotionRange(mapping.rightStickXAxis);
|
||||
InputDevice.MotionRange rsYRange = dev.getMotionRange(mapping.rightStickYAxis);
|
||||
if (rsXRange != null) {
|
||||
mapping.rightStickXAxisDeadzone = rsXRange.getFlat();
|
||||
}
|
||||
if (rsYRange != null) {
|
||||
mapping.rightStickYAxisDeadzone = rsYRange.getFlat();
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
||||
// Unknown devices can't be handled
|
||||
if (dev == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String descriptor = dev.getDescriptor();
|
||||
|
||||
// Return the existing mapping if it exists
|
||||
ControllerMapping mapping = mappings.get(descriptor);
|
||||
if (mapping != null) {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// Otherwise create a new mapping
|
||||
mapping = createMappingForDevice(dev);
|
||||
mappings.put(descriptor, mapping);
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket() {
|
||||
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
private int handleRemapping(ControllerMapping mapping, int keyCode) {
|
||||
if (mapping.isDualShock4) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_Z:
|
||||
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_C:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
|
||||
// These are duplicate trigger events
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||
switch (keyCode) {
|
||||
// These are duplicate dpad events for hat input
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return keyCode;
|
||||
}
|
||||
|
||||
public boolean handleMotionEvent(MotionEvent event) {
|
||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||
if (mapping == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle left stick events outside of the deadzone
|
||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||
float LS_X = event.getAxisValue(mapping.leftStickXAxis);
|
||||
float LS_Y = event.getAxisValue(mapping.leftStickYAxis);
|
||||
if (LS_X >= -mapping.leftStickXAxisDeadzone && LS_X <= mapping.leftStickXAxisDeadzone) {
|
||||
LS_X = 0;
|
||||
}
|
||||
if (LS_Y >= -mapping.leftStickYAxisDeadzone && LS_Y <= mapping.leftStickYAxisDeadzone) {
|
||||
LS_Y = 0;
|
||||
}
|
||||
leftStickX = (short)Math.round(LS_X * 0x7FFF);
|
||||
leftStickY = (short)Math.round(-LS_Y * 0x7FFF);
|
||||
}
|
||||
|
||||
// Handle right stick events outside of the deadzone
|
||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||
float RS_X = event.getAxisValue(mapping.rightStickXAxis);
|
||||
float RS_Y = event.getAxisValue(mapping.rightStickYAxis);
|
||||
if (RS_X >= -mapping.rightStickXAxisDeadzone && RS_X <= mapping.rightStickXAxisDeadzone) {
|
||||
RS_X = 0;
|
||||
}
|
||||
if (RS_Y >= -mapping.rightStickYAxisDeadzone && RS_Y <= mapping.rightStickYAxisDeadzone) {
|
||||
RS_Y = 0;
|
||||
}
|
||||
rightStickX = (short)Math.round(RS_X * 0x7FFF);
|
||||
rightStickY = (short)Math.round(-RS_Y * 0x7FFF);
|
||||
}
|
||||
|
||||
// Handle controllers with analog triggers
|
||||
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||
float L2 = event.getAxisValue(mapping.leftTriggerAxis);
|
||||
float R2 = event.getAxisValue(mapping.rightTriggerAxis);
|
||||
|
||||
if (mapping.triggersIdleNegative) {
|
||||
L2 = (L2 + 1) / 2;
|
||||
R2 = (R2 + 1) / 2;
|
||||
}
|
||||
|
||||
leftTrigger = (byte)Math.round(L2 * 0xFF);
|
||||
rightTrigger = (byte)Math.round(R2 * 0xFF);
|
||||
}
|
||||
|
||||
// Hats emulate d-pad events
|
||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||
|
||||
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||
if (hatX < -(0.5 + mapping.hatXDeadzone)) {
|
||||
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
}
|
||||
else if (hatX > (0.5 + mapping.hatXDeadzone)) {
|
||||
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
}
|
||||
|
||||
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||
if (hatY < -(0.5 + mapping.hatYDeadzone)) {
|
||||
inputMap |= ControllerPacket.UP_FLAG;
|
||||
}
|
||||
else if (hatY > (0.5 + mapping.hatYDeadzone)) {
|
||||
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
sendControllerInputPacket();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean handleButtonUp(int keyCode, KeyEvent event) {
|
||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||
if (mapping == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
keyCode = handleRemapping(mapping, keyCode);
|
||||
if (keyCode == 0) {
|
||||
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;
|
||||
break;
|
||||
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_DPAD_CENTER:
|
||||
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;
|
||||
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;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
leftTrigger = 0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
rightTrigger = 0;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're emulating the select button
|
||||
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||
{
|
||||
// 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();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean handleButtonDown(int keyCode, KeyEvent event) {
|
||||
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||
if (mapping == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
keyCode = handleRemapping(mapping, keyCode);
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
break;
|
||||
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_DPAD_CENTER:
|
||||
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;
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
leftTrigger = (byte)0xFF;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
rightTrigger = (byte)0xFF;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 | ControllerPacket.RB_FLAG);
|
||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
|
||||
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
}
|
||||
|
||||
sendControllerInputPacket();
|
||||
return true;
|
||||
}
|
||||
|
||||
class ControllerMapping {
|
||||
public int leftStickXAxis = -1;
|
||||
public float leftStickXAxisDeadzone;
|
||||
|
||||
public int leftStickYAxis = -1;
|
||||
public float leftStickYAxisDeadzone;
|
||||
|
||||
public int rightStickXAxis = -1;
|
||||
public float rightStickXAxisDeadzone;
|
||||
|
||||
public int rightStickYAxis = -1;
|
||||
public float rightStickYAxisDeadzone;
|
||||
|
||||
public int leftTriggerAxis = -1;
|
||||
public int rightTriggerAxis = -1;
|
||||
public boolean triggersIdleNegative;
|
||||
|
||||
public int hatXAxis = -1;
|
||||
public int hatYAxis = -1;
|
||||
public float hatXDeadzone;
|
||||
public float hatYDeadzone;
|
||||
|
||||
public boolean isDualShock4;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.limelight;
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||
|
||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
@@ -141,22 +142,23 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
public void start(final VideoDepacketizer depacketizer) {
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = System.currentTimeMillis();
|
||||
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
if (du != null) {
|
||||
submitDecodeUnit(du);
|
||||
}
|
||||
|
||||
long diff = nextFrameTime - System.currentTimeMillis();
|
||||
|
||||
if (diff > WAIT_CEILING_MS) {
|
||||
try {
|
||||
Thread.sleep(diff);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
@@ -165,6 +167,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (CPU)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
@@ -186,8 +189,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
|
||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
@@ -26,8 +26,8 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
decoderRenderer.start();
|
||||
public void start(VideoDepacketizer depacketizer) {
|
||||
decoderRenderer.start(depacketizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,11 +35,6 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
decoderRenderer.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit du) {
|
||||
return decoderRenderer.submitDecodeUnit(du);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return decoderRenderer.getCapabilities();
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
@@ -23,29 +24,27 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
private ByteBuffer[] videoDecoderInputBuffers;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
private int redrawRate;
|
||||
private boolean needsSpsFixup;
|
||||
private boolean fastInputQueueing;
|
||||
private boolean needsSpsBitstreamFixup;
|
||||
private boolean needsSpsNumRefFixup;
|
||||
private VideoDepacketizer depacketizer;
|
||||
|
||||
public static final List<String> blacklistedDecoderPrefixes;
|
||||
public static final List<String> spsFixupDecoderPrefixes;
|
||||
public static final List<String> fastInputQueueingPrefixes;
|
||||
public static final List<String> spsFixupBitsreamFixupDecoderPrefixes;
|
||||
public static final List<String> spsFixupNumRefFixupDecoderPrefixes;
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||
|
||||
// TI's decoder technically supports high profile but doesn't work for some reason
|
||||
blacklistedDecoderPrefixes.add("omx.TI");
|
||||
// Nothing here right now :)
|
||||
}
|
||||
|
||||
static {
|
||||
spsFixupDecoderPrefixes = new LinkedList<String>();
|
||||
spsFixupDecoderPrefixes.add("omx.nvidia");
|
||||
}
|
||||
|
||||
static {
|
||||
fastInputQueueingPrefixes = new LinkedList<String>();
|
||||
fastInputQueueingPrefixes.add("omx.nvidia");
|
||||
spsFixupBitsreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||
spsFixupBitsreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||
|
||||
spsFixupNumRefFixupDecoderPrefixes = new LinkedList<String>();
|
||||
spsFixupNumRefFixupDecoderPrefixes.add("omx.TI");
|
||||
spsFixupNumRefFixupDecoderPrefixes.add("omx.qcom");
|
||||
}
|
||||
|
||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||
@@ -120,27 +119,25 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.redrawRate = redrawRate;
|
||||
|
||||
dumpDecoders();
|
||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
//dumpDecoders();
|
||||
|
||||
MediaCodecInfo safeDecoder = findSafeDecoder();
|
||||
if (safeDecoder != null) {
|
||||
videoDecoder = MediaCodec.createByCodecName(safeDecoder.getName());
|
||||
needsSpsFixup = isDecoderInList(spsFixupDecoderPrefixes, safeDecoder.getName());
|
||||
if (needsSpsFixup) {
|
||||
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS fixup");
|
||||
needsSpsBitstreamFixup = isDecoderInList(spsFixupBitsreamFixupDecoderPrefixes, safeDecoder.getName());
|
||||
needsSpsNumRefFixup = isDecoderInList(spsFixupNumRefFixupDecoderPrefixes, safeDecoder.getName());
|
||||
if (needsSpsBitstreamFixup) {
|
||||
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS bitstream restrictions fixup");
|
||||
}
|
||||
fastInputQueueing = isDecoderInList(fastInputQueueingPrefixes, safeDecoder.getName());
|
||||
if (fastInputQueueing) {
|
||||
LimeLog.info("Decoder "+safeDecoder.getName()+" supports fast input queueing");
|
||||
if (needsSpsNumRefFixup) {
|
||||
LimeLog.info("Decoder "+safeDecoder.getName()+" needs SPS ref num fixup");
|
||||
}
|
||||
}
|
||||
else {
|
||||
videoDecoder = MediaCodec.createDecoderByType("video/avc");
|
||||
needsSpsFixup = false;
|
||||
fastInputQueueing = false;
|
||||
needsSpsBitstreamFixup = false;
|
||||
needsSpsNumRefFixup = false;
|
||||
}
|
||||
|
||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
||||
@@ -158,59 +155,51 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTimeUs = 0;
|
||||
BufferInfo info = new BufferInfo();
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
// Block for a maximum of 100 ms
|
||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 100000);
|
||||
switch (outIndex) {
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
LimeLog.info("Output buffers changed");
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
if (du != null) {
|
||||
submitDecodeUnit(du);
|
||||
}
|
||||
|
||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
|
||||
if (outIndex >= 0) {
|
||||
int lastIndex = outIndex;
|
||||
boolean render = false;
|
||||
|
||||
if (currentTimeUs() >= nextFrameTimeUs) {
|
||||
render = true;
|
||||
nextFrameTimeUs = computePresentationTime(redrawRate);
|
||||
}
|
||||
|
||||
// Get the last output buffer in the queue
|
||||
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||
lastIndex = outIndex;
|
||||
}
|
||||
|
||||
// Render that buffer if it's time for the next frame
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, render);
|
||||
|
||||
// Render the last buffer
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
} else {
|
||||
switch (outIndex) {
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
LimeLog.info("Output buffers changed");
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
private static long currentTimeUs() {
|
||||
return System.nanoTime() / 1000;
|
||||
}
|
||||
|
||||
private long computePresentationTime(int frameRate) {
|
||||
return currentTimeUs() + (1000000 / frameRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
public void start(VideoDepacketizer depacketizer) {
|
||||
this.depacketizer = depacketizer;
|
||||
startRendererThread();
|
||||
}
|
||||
|
||||
@@ -230,24 +219,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
if (decodeUnit.getType() != DecodeUnit.TYPE_H264) {
|
||||
System.err.println("Unknown decode unit type");
|
||||
return false;
|
||||
}
|
||||
|
||||
int mcFlags = 0;
|
||||
|
||||
if ((decodeUnit.getFlags() & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||
LimeLog.info("Codec config");
|
||||
mcFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||
}
|
||||
if ((decodeUnit.getFlags() & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
|
||||
LimeLog.info("Sync frame");
|
||||
mcFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
||||
}
|
||||
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||
if (inputIndex >= 0)
|
||||
{
|
||||
@@ -256,44 +228,54 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
// Clear old input data
|
||||
buf.clear();
|
||||
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||
// We manually modify the SPS here to speed-up decoding if the decoder was flagged as needing it.
|
||||
if (needsSpsFixup) {
|
||||
if (needsSpsBitstreamFixup || needsSpsNumRefFixup) {
|
||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
||||
// Check for SPS NALU type
|
||||
if (header.data[header.offset+4] == 0x67) {
|
||||
int spsLength;
|
||||
// TI OMAP4 requires a reference frame count of 1 to decode successfully
|
||||
if (needsSpsNumRefFixup) {
|
||||
LimeLog.info("Fixing up num ref frames");
|
||||
this.replace(header, 80, 9, new byte[] {0x40}, 3);
|
||||
}
|
||||
|
||||
switch (header.length) {
|
||||
case 26:
|
||||
LimeLog.info("Modifying SPS (26)");
|
||||
buf.put(header.data, header.offset, 24);
|
||||
buf.put((byte) 0x11);
|
||||
buf.put((byte) 0xe3);
|
||||
buf.put((byte) 0x06);
|
||||
buf.put((byte) 0x50);
|
||||
spsLength = header.length + 2;
|
||||
break;
|
||||
case 27:
|
||||
LimeLog.info("Modifying SPS (27)");
|
||||
buf.put(header.data, header.offset, 25);
|
||||
buf.put((byte) 0x04);
|
||||
buf.put((byte) 0x78);
|
||||
buf.put((byte) 0xc1);
|
||||
buf.put((byte) 0x94);
|
||||
spsLength = header.length + 2;
|
||||
break;
|
||||
default:
|
||||
LimeLog.warning("Unknown SPS of length "+header.length);
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||
// We manually modify the SPS here to speed-up decoding if the decoder was flagged as needing it.
|
||||
int spsLength;
|
||||
if (needsSpsBitstreamFixup) {
|
||||
switch (header.length) {
|
||||
case 26:
|
||||
LimeLog.info("Adding bitstream restrictions to SPS (26)");
|
||||
buf.put(header.data, header.offset, 24);
|
||||
buf.put((byte) 0x11);
|
||||
buf.put((byte) 0xe3);
|
||||
buf.put((byte) 0x06);
|
||||
buf.put((byte) 0x50);
|
||||
spsLength = header.length + 2;
|
||||
break;
|
||||
case 27:
|
||||
LimeLog.info("Adding bitstream restrictions to SPS (27)");
|
||||
buf.put(header.data, header.offset, 25);
|
||||
buf.put((byte) 0x04);
|
||||
buf.put((byte) 0x78);
|
||||
buf.put((byte) 0xc1);
|
||||
buf.put((byte) 0x94);
|
||||
spsLength = header.length + 2;
|
||||
break;
|
||||
default:
|
||||
LimeLog.warning("Unknown SPS of length "+header.length);
|
||||
buf.put(header.data, header.offset, header.length);
|
||||
spsLength = header.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
buf.put(header.data, header.offset, header.length);
|
||||
spsLength = header.length;
|
||||
break;
|
||||
}
|
||||
|
||||
videoDecoder.queueInputBuffer(inputIndex,
|
||||
0, spsLength,
|
||||
0, mcFlags);
|
||||
0, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -306,7 +288,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
videoDecoder.queueInputBuffer(inputIndex,
|
||||
0, decodeUnit.getDataLength(),
|
||||
0, mcFlags);
|
||||
0, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -314,6 +296,85 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return fastInputQueueing ? VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace bits in array
|
||||
* @param source array in which bits should be replaced
|
||||
* @param srcOffset offset in bits where replacement should take place
|
||||
* @param srcLength length in bits of data that should be replaced
|
||||
* @param data data array with the the replacement data
|
||||
* @param dataLength length of replacement data in bits
|
||||
*/
|
||||
public void replace(ByteBufferDescriptor source, int srcOffset, int srcLength, byte[] data, int dataLength) {
|
||||
//Add 7 to always round up
|
||||
int length = (source.length*8-srcLength+dataLength+7)/8;
|
||||
|
||||
int bitOffset = srcOffset%8;
|
||||
int byteOffset = srcOffset/8;
|
||||
|
||||
byte dest[] = null;
|
||||
int offset = 0;
|
||||
if (length>source.length) {
|
||||
dest = new byte[length];
|
||||
|
||||
//Copy the first bytes
|
||||
System.arraycopy(source.data, source.offset, dest, offset, byteOffset);
|
||||
} else {
|
||||
dest = source.data;
|
||||
offset = source.offset;
|
||||
}
|
||||
|
||||
int byteLength = (bitOffset+dataLength+7)/8;
|
||||
int bitTrailing = 8 - (srcOffset+dataLength) % 8;
|
||||
for (int i=0;i<byteLength;i++) {
|
||||
byte result = 0;
|
||||
if (i != 0)
|
||||
result = (byte) (data[i-1] << 8-bitOffset);
|
||||
else if (bitOffset > 0)
|
||||
result = (byte) (source.data[byteOffset+source.offset] & (0xFF << 8-bitOffset));
|
||||
|
||||
if (i == 0 || i != byteLength-1) {
|
||||
byte moved = (byte) ((data[i]&0xFF) >>> bitOffset);
|
||||
result |= moved;
|
||||
}
|
||||
|
||||
if (i == byteLength-1 && bitTrailing > 0) {
|
||||
int sourceOffset = srcOffset+srcLength/8;
|
||||
int bitMove = (dataLength-srcLength)%8;
|
||||
if (bitMove<0) {
|
||||
result |= (byte) (source.data[sourceOffset+source.offset] << -bitMove & (0xFF >>> bitTrailing));
|
||||
result |= (byte) (source.data[sourceOffset+1+source.offset] << -bitMove & (0xFF >>> 8+bitMove));
|
||||
} else {
|
||||
byte moved = (byte) ((source.data[sourceOffset+source.offset]&0xFF) >>> bitOffset);
|
||||
result |= moved;
|
||||
}
|
||||
}
|
||||
|
||||
dest[i+byteOffset+offset] = result;
|
||||
}
|
||||
|
||||
//Source offset
|
||||
byteOffset += srcLength/8;
|
||||
bitOffset = (srcOffset+dataLength-srcLength)%8;
|
||||
|
||||
//Offset in destination
|
||||
int destOffset = (srcOffset+dataLength)/8;
|
||||
|
||||
for (int i=1;i<source.length-byteOffset;i++) {
|
||||
int diff = destOffset >= byteOffset-1?i:source.length-byteOffset-i;
|
||||
|
||||
byte result = 0;
|
||||
result = (byte) (source.data[byteOffset+diff-1+source.offset] << 8-bitOffset);
|
||||
byte moved = (byte) ((source.data[byteOffset+diff+source.offset]&0xFF) >>> bitOffset);
|
||||
result ^= moved;
|
||||
|
||||
dest[diff+destOffset+offset] = result;
|
||||
}
|
||||
|
||||
source.data = dest;
|
||||
source.offset = offset;
|
||||
source.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user