Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32af2d0831 | |||
| 242b03d4b5 | |||
| 87a62666ac | |||
| 2dcf5486da | |||
| 60d3d8b3ae | |||
| e9141d65fe | |||
| aae591daec | |||
| a5ca8a7472 | |||
| 36f8cc02cb | |||
| 55b9645651 | |||
| d30ecbed5b | |||
| 0bbd27f04c | |||
| 3c53fb7403 | |||
| 7a81950819 | |||
| 74f212c702 | |||
| 36be943854 | |||
| 26a4fc75a5 | |||
| a5ec5fc265 | |||
| 541ac44be4 | |||
| 117b555fcd | |||
| a10cd04441 | |||
| 53dccbde2a | |||
| 56625dfe4b | |||
| 2eab5a3b7b | |||
| f9e811862a | |||
| 25ccc3d0e1 | |||
| 8853bf0670 | |||
| 71fa3a824b | |||
| 56fd50834c | |||
| 48ba812cf6 | |||
| 019dc6d45f | |||
| cbcb784a79 | |||
| 39fa0258ad | |||
| d0dd5bfa8c | |||
| b948c47618 | |||
| 18cae8ac53 | |||
| 0576231dfc | |||
| 6ad35a83dd | |||
| 33d4dfc745 | |||
| f3bf63a668 | |||
| 2dbb7395a4 | |||
| 7c1eb80d62 | |||
| f2bf093691 | |||
| 2f002bfa4a | |||
| 4a19038d54 | |||
| 15fb3dd92c | |||
| e0982d3961 | |||
| 7fb2f15f54 | |||
| f93dbb4116 | |||
| bc34fe3a9f | |||
| bbe49491c1 | |||
| d5ccb80f26 | |||
| 50fd15379a | |||
| ed479f1155 | |||
| 04db9ba714 | |||
| 6a973e3248 | |||
| 96d9e4977b | |||
| 5a3897f22a | |||
| ceef00b79a | |||
| 94ee24ea11 | |||
| 1a201f2e94 | |||
| e0c6d41d4b | |||
| 44a0ae86d2 | |||
| 06822ad385 | |||
| 3be52280ba | |||
| 5142f978cf | |||
| 667ffd4dfd | |||
| 17626f1853 | |||
| 5c79567a2c | |||
| 0f5fd9af62 | |||
| 99643537d1 | |||
| 47650386e0 | |||
| aa3fc34646 | |||
| 92f5f1ac71 | |||
| eb739f73c7 | |||
| 20a646106b | |||
| 0dc14517cd | |||
| 04713c007b | |||
| 1cac7660b8 | |||
| edb286f9af | |||
| fb15ff99ca | |||
| a455e75e37 | |||
| 2b452e51f9 | |||
| 9d2b6f8854 | |||
| 3be10a1b59 | |||
| 01950c25a8 | |||
| 7ad1ebd0e8 | |||
| ee01a8b5a0 | |||
| 23c54f6813 | |||
| ceef4510fb |
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "app/src/main/jni/jnienet/enet"]
|
||||||
|
path = app/src/main/jni/jnienet/enet
|
||||||
|
url = https://github.com/cgutman/enet.git
|
||||||
@@ -8,7 +8,7 @@ whether in your own home or over the internet.
|
|||||||
|
|
||||||
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
##Features
|
||||||
|
|
||||||
|
|||||||
+16
-5
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion 24
|
||||||
buildToolsVersion "23.0.2"
|
buildToolsVersion "24.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 24
|
||||||
|
|
||||||
versionName "4.5"
|
versionName "4.6"
|
||||||
versionCode = 84
|
versionCode = 103
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
@@ -25,6 +25,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
@@ -32,6 +36,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These lines are required to avoid dexing issues with the BouncyCastle library
|
||||||
|
// bundled with limelight-common.jar
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/BCKEY.SF'
|
||||||
|
exclude 'META-INF/BCKEY.DSA'
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets.main.jni.srcDirs = []
|
sourceSets.main.jni.srcDirs = []
|
||||||
|
|
||||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -12,17 +12,18 @@
|
|||||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:banner="@drawable/atv_banner"
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
|
|
||||||
<!-- Launcher for traditional devices -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
android:name=".PcView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
@@ -30,22 +31,10 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Launcher for Android TV devices -->
|
|
||||||
<activity
|
|
||||||
android:name=".PcViewTv"
|
|
||||||
android:logo="@drawable/atv_banner"
|
|
||||||
android:icon="@drawable/atv_banner"
|
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppView"
|
android:name=".AppView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private String lastRawApplist;
|
private String lastRawApplist;
|
||||||
private int lastRunningAppId;
|
private int lastRunningAppId;
|
||||||
private boolean suspendGridUpdates;
|
private boolean suspendGridUpdates;
|
||||||
|
private boolean inForeground;
|
||||||
|
|
||||||
private final static int START_OR_RESUME_ID = 1;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
@@ -95,9 +96,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Load the app grid with cached data (if possible)
|
// Load the app grid with cached data (if possible)
|
||||||
populateAppGridWithCache();
|
populateAppGridWithCache();
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
runOnUiThread(new Runnable() {
|
||||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
@Override
|
||||||
.commitAllowingStateLoss();
|
public void run() {
|
||||||
|
if (isFinishing() || isChangingConfigurations()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Despite my best efforts to catch all conditions that could
|
||||||
|
// cause the activity to be destroyed when we try to commit
|
||||||
|
// I haven't been able to, so we have this try-catch block.
|
||||||
|
try {
|
||||||
|
getFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
@@ -108,7 +125,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder == null) {
|
// Don't start polling if we're not bound or in the foreground
|
||||||
|
if (managerBinder == null || !inForeground) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +270,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +278,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
|
inForeground = false;
|
||||||
stopComputerUpdates();
|
stopComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ package com.limelight;
|
|||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureManager;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
import com.limelight.binding.input.TouchContext;
|
import com.limelight.binding.input.TouchContext;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.binding.input.evdev.EvdevHandler;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
import com.limelight.binding.input.virtual_controller.VirtualController;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
||||||
@@ -21,6 +22,7 @@ import com.limelight.nvstream.input.KeyboardPacket;
|
|||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ import android.hardware.input.InputManager;
|
|||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -46,12 +49,10 @@ import android.view.InputDevice;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.SurfaceView;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnGenericMotionListener;
|
import android.view.View.OnGenericMotionListener;
|
||||||
import android.view.View.OnSystemUiVisibilityChangeListener;
|
import android.view.View.OnSystemUiVisibilityChangeListener;
|
||||||
import android.view.View.OnTouchListener;
|
import android.view.View.OnTouchListener;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
@@ -73,8 +74,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
private long threeFingerDownTime = 0;
|
private long threeFingerDownTime = 0;
|
||||||
|
|
||||||
private static final double REFERENCE_HORIZ_RES = 1280;
|
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||||
private static final double REFERENCE_VERT_RES = 720;
|
private static final int REFERENCE_VERT_RES = 720;
|
||||||
|
|
||||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
|
|
||||||
@@ -83,19 +84,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private KeyboardTranslator keybTranslator;
|
private KeyboardTranslator keybTranslator;
|
||||||
|
|
||||||
private PreferenceConfiguration prefConfig;
|
private PreferenceConfiguration prefConfig;
|
||||||
private final Point screenSize = new Point(0, 0);
|
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private SpinnerDialog spinner;
|
private SpinnerDialog spinner;
|
||||||
private boolean displayedFailureDialog = false;
|
private boolean displayedFailureDialog = false;
|
||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
private boolean deferredSurfaceResize = false;
|
|
||||||
|
|
||||||
private EvdevHandler evdevHandler;
|
private InputCaptureProvider inputCaptureProvider;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
|
private StreamView streamView;
|
||||||
|
|
||||||
private EnhancedDecoderRenderer decoderRenderer;
|
private EnhancedDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
@@ -174,13 +174,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
|
||||||
display.getSize(screenSize);
|
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
|
streamView = (StreamView) findViewById(R.id.surfaceView);
|
||||||
sv.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
sv.setOnTouchListener(this);
|
streamView.setOnTouchListener(this);
|
||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
@@ -249,44 +246,24 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||||
|
|
||||||
boolean aspectRatioMatch = false;
|
// Set to the optimal mode for streaming
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
prepareDisplayForRendering();
|
||||||
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
|
||||||
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
|
||||||
// setFixedSize so we can handle 4K properly. The only known devices that have
|
|
||||||
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
|
||||||
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
|
||||||
// 4K devices run 6.0 at the moment.
|
|
||||||
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
|
||||||
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
|
||||||
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
|
||||||
LimeLog.info("Stream has compatible aspect ratio with output display");
|
|
||||||
aspectRatioMatch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SurfaceHolder sh = sv.getHolder();
|
|
||||||
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
|
||||||
// Set the surface to the size of the video
|
|
||||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
deferredSurfaceResize = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i,
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
(REFERENCE_HORIZ_RES / (double)screenSize.x),
|
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||||
(REFERENCE_VERT_RES / (double)screenSize.y));
|
streamView);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LimelightBuildProps.ROOT_BUILD) {
|
// Use sustained performance mode on N+ to ensure consistent
|
||||||
// Start watching for raw input
|
// CPU availability
|
||||||
evdevHandler = new EvdevHandler(this, this);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
evdevHandler.start();
|
getWindow().setSustainedPerformanceMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||||
|
|
||||||
if (prefConfig.onscreenController) {
|
if (prefConfig.onscreenController) {
|
||||||
// create virtual onscreen controller
|
// create virtual onscreen controller
|
||||||
virtualController = new VirtualController(conn,
|
virtualController = new VirtualController(conn,
|
||||||
@@ -302,26 +279,95 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The connection will be started when the surface gets created
|
// The connection will be started when the surface gets created
|
||||||
sh.addCallback(this);
|
streamView.getHolder().addCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
|
private void prepareDisplayForRendering() {
|
||||||
{
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
// Get the visible width of the activity
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
|
||||||
|
|
||||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
// On M, we can explicitly set the optimal display mode
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Display.Mode bestMode = display.getMode();
|
||||||
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
|
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
||||||
|
candidate.getRefreshRate() < 63;
|
||||||
|
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||||
|
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||||
|
candidate.getPhysicalWidth() <= 4096;
|
||||||
|
|
||||||
// Calculate the new size of the SurfaceView
|
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
||||||
lp.width = (int) visibleWidth;
|
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
||||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
|
||||||
|
|
||||||
// Apply the size change
|
// On non-4K streams, we force the resolution to never change
|
||||||
sv.setLayoutParams(lp);
|
if (prefConfig.width < 3840) {
|
||||||
|
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
||||||
|
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// refresh virtual controller layout
|
// Make sure the refresh rate doesn't regress
|
||||||
if (virtualController != null) {
|
if (!refreshRateOk) {
|
||||||
virtualController.refreshLayout();
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the resolution doesn't regress
|
||||||
|
if (!resolutionOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestMode = candidate;
|
||||||
|
}
|
||||||
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
|
}
|
||||||
|
// On L, we can at least tell the OS that we want 60 Hz
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
float bestRefreshRate = display.getRefreshRate();
|
||||||
|
for (float candidate : display.getSupportedRefreshRates()) {
|
||||||
|
if (candidate > bestRefreshRate && candidate < 63) {
|
||||||
|
LimeLog.info("Examining refresh rate: "+candidate);
|
||||||
|
bestRefreshRate = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||||
|
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the display mode change
|
||||||
|
getWindow().setAttributes(windowLayoutParams);
|
||||||
|
|
||||||
|
// From 4.4 to 5.1 we can't ask for a 4K display mode, so we'll
|
||||||
|
// need to hint the OS to provide one.
|
||||||
|
boolean aspectRatioMatch = false;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
|
||||||
|
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
||||||
|
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
||||||
|
// setFixedSize so we can handle 4K properly. The only known devices that have
|
||||||
|
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||||
|
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||||
|
// 4K devices run 6.0 at the moment.
|
||||||
|
Point screenSize = new Point(0, 0);
|
||||||
|
display.getSize(screenSize);
|
||||||
|
|
||||||
|
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||||
|
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||||
|
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||||
|
LimeLog.info("Stream has compatible aspect ratio with output display");
|
||||||
|
aspectRatioMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
||||||
|
// Set the surface to the size of the video
|
||||||
|
streamView.getHolder().setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set the surface to scale based on the aspect ratio of the stream
|
||||||
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,13 +468,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private final Runnable toggleGrab = new Runnable() {
|
private final Runnable toggleGrab = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (evdevHandler != null) {
|
if (grabbedInput) {
|
||||||
if (grabbedInput) {
|
inputCaptureProvider.disableCapture();
|
||||||
evdevHandler.ungrabAll();
|
}
|
||||||
}
|
else {
|
||||||
else {
|
inputCaptureProvider.enableCapture();
|
||||||
evdevHandler.regrabAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grabbedInput = !grabbedInput;
|
grabbedInput = !grabbedInput;
|
||||||
@@ -535,11 +579,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eat repeat down events
|
|
||||||
if (event.getRepeatCount() > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
@@ -616,7 +655,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||||
{
|
{
|
||||||
// This case is for mice
|
// This case is for mice
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
|
(event.getPointerCount() >= 1 &&
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||||
|
|
||||||
@@ -653,13 +694,26 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First process the history
|
// Get relative axis values if we can
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||||
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
// Send the deltas straight from the motion event
|
||||||
}
|
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
|
||||||
|
(short) inputCaptureProvider.getRelativeAxisY(event));
|
||||||
|
|
||||||
// Now process the current values
|
// We have to also update the position Android thinks the cursor is at
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
// in order to avoid jumping when we stop moving or click.
|
||||||
|
lastMouseX = (int)event.getX();
|
||||||
|
lastMouseY = (int)event.getY();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// First process the history
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
}
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
lastButtonState = event.getButtonState();
|
||||||
}
|
}
|
||||||
@@ -782,8 +836,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Scale the deltas if the device resolution is different
|
// Scale the deltas if the device resolution is different
|
||||||
// than the stream resolution
|
// than the stream resolution
|
||||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)screenSize.x));
|
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
|
||||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)screenSize.y));
|
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
@@ -821,11 +875,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
conn.stop();
|
conn.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the Evdev reader to allow use of captured input devices
|
// Enable cursor visibility again
|
||||||
if (evdevHandler != null) {
|
inputCaptureProvider.disableCapture();
|
||||||
evdevHandler.stop();
|
|
||||||
evdevHandler = null;
|
// Destroy the capture provider
|
||||||
}
|
inputCaptureProvider.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -865,6 +919,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
connecting = false;
|
connecting = false;
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Hide the mouse cursor now. Doing it before
|
||||||
|
// dismissing the spinner seems to be undone
|
||||||
|
// when the spinner gets displayed.
|
||||||
|
inputCaptureProvider.enableCapture();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
hideSystemUi(1000);
|
hideSystemUi(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,13 +963,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (!connected && !connecting) {
|
if (!connected && !connecting) {
|
||||||
connecting = true;
|
connecting = true;
|
||||||
|
|
||||||
// Resize the surface to match the aspect ratio of the video
|
|
||||||
// This must be done after the surface is created.
|
|
||||||
if (deferredSurfaceResize) {
|
|
||||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
|
||||||
prefConfig.width, prefConfig.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||||
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
||||||
}
|
}
|
||||||
@@ -914,6 +971,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
// HACK: Android is supposed to let you return from this function
|
||||||
|
// before throwing a fit if you access the surface again. Unfortunately,
|
||||||
|
// MediaCodec often tries to access the destroyed surface and triggers
|
||||||
|
// an IllegalStateException. To workaround this, we will invoke
|
||||||
|
// the DecoderRenderer's stop function ourselves, so it will hopefully
|
||||||
|
// happen early enough to not trigger the bug
|
||||||
|
decoderRenderer.stop();
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private RelativeLayout noPcFoundLayout;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling, hasResumed;
|
private boolean freezeUpdates, runningPolling, inForeground;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
@@ -161,11 +161,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder != null) {
|
// Only allow polling to start if we're bound to CMS, polling is not already running,
|
||||||
if (runningPolling) {
|
// and our activity is in the foreground.
|
||||||
return;
|
if (managerBinder != null && !runningPolling && inForeground) {
|
||||||
}
|
|
||||||
|
|
||||||
freezeUpdates = false;
|
freezeUpdates = false;
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -215,7 +213,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
hasResumed = true;
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +221,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
hasResumed = false;
|
inForeground = false;
|
||||||
stopComputerUpdates(false);
|
stopComputerUpdates(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +269,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
||||||
// We don't want to start computer updates again, so we need to keep track of whether we're paused.
|
// startComputerUpdates() manages this and won't actual start polling until the activity
|
||||||
if (hasResumed) {
|
// returns to the foreground.
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
@@ -302,7 +299,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
InetAddress addr = null;
|
InetAddress addr;
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
addr = computer.localIp;
|
addr = computer.localIp;
|
||||||
}
|
}
|
||||||
@@ -330,7 +327,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||||
|
|
||||||
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||||
}
|
}
|
||||||
@@ -368,14 +365,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toastSuccess) {
|
if (toastSuccess) {
|
||||||
// Open the app list after a successful pairing attemp
|
// Open the app list after a successful pairing attempt
|
||||||
doAppList(computer);
|
doAppList(computer);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Start polling again if we're still in the foreground
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start polling again
|
|
||||||
startComputerUpdates();
|
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -431,7 +429,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
InetAddress addr = null;
|
InetAddress addr;
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
addr = computer.localIp;
|
addr = computer.localIp;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
/* This is a dummy class to allow for a separate icon
|
|
||||||
* and launcher for TV.
|
|
||||||
*/
|
|
||||||
public class PcViewTv extends PcView {}
|
|
||||||
@@ -82,6 +82,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
defaultContext.controllerNumber = (short) 0;
|
defaultContext.controllerNumber = (short) 0;
|
||||||
|
defaultContext.assignedControllerNumber = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
@@ -146,8 +147,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
InputDeviceContext devContext = (InputDeviceContext) context;
|
InputDeviceContext devContext = (InputDeviceContext) context;
|
||||||
|
|
||||||
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
||||||
if (devContext.name != null && devContext.name.contains("gpio-keys")) {
|
if (devContext.name != null &&
|
||||||
// This is the back button on Shield portable consoles
|
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
|
||||||
|
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
|
||||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
context.controllerNumber = 0;
|
context.controllerNumber = 0;
|
||||||
}
|
}
|
||||||
@@ -212,6 +214,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
|
|
||||||
LimeLog.info("Creating controller context for device: "+devName);
|
LimeLog.info("Creating controller context for device: "+devName);
|
||||||
|
LimeLog.info(dev.toString());
|
||||||
|
|
||||||
context.name = devName;
|
context.name = devName;
|
||||||
context.id = dev.getId();
|
context.id = dev.getId();
|
||||||
@@ -229,6 +232,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||||
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||||
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||||
|
InputDevice.MotionRange throttleRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_THROTTLE);
|
||||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||||
{
|
{
|
||||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||||
@@ -241,6 +245,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
}
|
}
|
||||||
|
else if (brakeRange != null && throttleRange != null)
|
||||||
|
{
|
||||||
|
// Others use THROTTLE and BRAKE (like Xiaomi)
|
||||||
|
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
|
context.rightTriggerAxis = MotionEvent.AXIS_THROTTLE;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
@@ -324,8 +334,22 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
||||||
if (hasSelectKey[0] && hasSelectKey[1]) {
|
if (hasSelectKey[0] && hasSelectKey[1]) {
|
||||||
LimeLog.info("Ignoring back button because select is present");
|
// Xiaomi gamepads claim to have both buttons then only send KEYCODE_BACK events
|
||||||
context.ignoreBack = true;
|
if (dev.getVendorId() != 0x2717) {
|
||||||
|
LimeLog.info("Ignoring back button because select is present");
|
||||||
|
context.ignoreBack = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
// The device name provided is just "Gamepad" which is pretty useless, so we
|
||||||
|
// use VID/PID instead
|
||||||
|
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
|
||||||
|
context.backIsStart = true;
|
||||||
|
context.modeIsSelect = true;
|
||||||
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,12 +422,82 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendControllerInputPacket(GenericControllerContext context) {
|
private byte maxByMagnitude(byte a, byte b) {
|
||||||
assignControllerNumberIfNeeded(context);
|
int absA = Math.abs(a);
|
||||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
int absB = Math.abs(b);
|
||||||
context.leftTrigger, context.rightTrigger,
|
if (absA > absB) {
|
||||||
context.leftStickX, context.leftStickY,
|
return a;
|
||||||
context.rightStickX, context.rightStickY);
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private short maxByMagnitude(short a, short b) {
|
||||||
|
int absA = Math.abs(a);
|
||||||
|
int absB = Math.abs(b);
|
||||||
|
if (absA > absB) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControllerInputPacket(GenericControllerContext originalContext) {
|
||||||
|
assignControllerNumberIfNeeded(originalContext);
|
||||||
|
|
||||||
|
// Take the context's controller number and fuse all inputs with the same number
|
||||||
|
short controllerNumber = originalContext.controllerNumber;
|
||||||
|
short inputMap = 0;
|
||||||
|
byte leftTrigger = 0;
|
||||||
|
byte rightTrigger = 0;
|
||||||
|
short leftStickX = 0;
|
||||||
|
short leftStickY = 0;
|
||||||
|
short rightStickX = 0;
|
||||||
|
short rightStickY = 0;
|
||||||
|
|
||||||
|
// In order to properly handle controllers that are split into multiple devices,
|
||||||
|
// we must aggregate all controllers with the same controller number into a single
|
||||||
|
// device before we send it.
|
||||||
|
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||||
|
GenericControllerContext context = inputDeviceContexts.valueAt(i);
|
||||||
|
if (context.assignedControllerNumber && context.controllerNumber == controllerNumber) {
|
||||||
|
inputMap |= context.inputMap;
|
||||||
|
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
|
||||||
|
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
|
||||||
|
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
|
||||||
|
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
|
||||||
|
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
|
||||||
|
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||||
|
GenericControllerContext context = usbDeviceContexts.valueAt(i);
|
||||||
|
if (context.assignedControllerNumber && context.controllerNumber == controllerNumber) {
|
||||||
|
inputMap |= context.inputMap;
|
||||||
|
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
|
||||||
|
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
|
||||||
|
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
|
||||||
|
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
|
||||||
|
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
|
||||||
|
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaultContext.controllerNumber == controllerNumber) {
|
||||||
|
inputMap |= defaultContext.inputMap;
|
||||||
|
leftTrigger |= maxByMagnitude(leftTrigger, defaultContext.leftTrigger);
|
||||||
|
rightTrigger |= maxByMagnitude(rightTrigger, defaultContext.rightTrigger);
|
||||||
|
leftStickX |= maxByMagnitude(leftStickX, defaultContext.leftStickX);
|
||||||
|
leftStickY |= maxByMagnitude(leftStickY, defaultContext.leftStickY);
|
||||||
|
rightStickX |= maxByMagnitude(rightStickX, defaultContext.rightStickX);
|
||||||
|
rightStickY |= maxByMagnitude(rightStickY, defaultContext.rightStickY);
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.sendControllerInput(controllerNumber, inputMap,
|
||||||
|
leftTrigger, rightTrigger,
|
||||||
|
leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
@@ -17,23 +19,27 @@ public class TouchContext {
|
|||||||
private boolean confirmedDrag;
|
private boolean confirmedDrag;
|
||||||
private Timer dragTimer;
|
private Timer dragTimer;
|
||||||
private double distanceMoved;
|
private double distanceMoved;
|
||||||
|
private double xFactor, yFactor;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final int actionIndex;
|
private final int actionIndex;
|
||||||
private final double xFactor;
|
private final int referenceWidth;
|
||||||
private final double yFactor;
|
private final int referenceHeight;
|
||||||
|
private final View targetView;
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
public TouchContext(NvConnection conn, int actionIndex,
|
||||||
|
int referenceWidth, int referenceHeight, View view)
|
||||||
{
|
{
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.actionIndex = actionIndex;
|
this.actionIndex = actionIndex;
|
||||||
this.xFactor = xFactor;
|
this.referenceWidth = referenceWidth;
|
||||||
this.yFactor = yFactor;
|
this.referenceHeight = referenceHeight;
|
||||||
|
this.targetView = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getActionIndex()
|
public int getActionIndex()
|
||||||
@@ -68,6 +74,10 @@ public class TouchContext {
|
|||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
|
// Get the view dimensions to scale inputs on this touch
|
||||||
|
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||||
|
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
originalTouchX = lastTouchX = eventX;
|
||||||
originalTouchY = lastTouchY = eventY;
|
originalTouchY = lastTouchY = eventY;
|
||||||
originalTouchTime = System.currentTimeMillis();
|
originalTouchTime = System.currentTimeMillis();
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.PointerIcon;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
public class AndroidCaptureProvider extends InputCaptureProvider {
|
||||||
|
private ViewGroup rootViewGroup;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public AndroidCaptureProvider(Activity activity) {
|
||||||
|
this.context = activity;
|
||||||
|
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPointerIconOnAllViews(PointerIcon icon) {
|
||||||
|
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
||||||
|
View view = rootViewGroup.getChildAt(i);
|
||||||
|
view.setPointerIcon(icon);
|
||||||
|
}
|
||||||
|
rootViewGroup.setPointerIcon(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
setPointerIconOnAllViews(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
||||||
|
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
|
public class InputCaptureManager {
|
||||||
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
|
// Shield capture is preferred because it can capture when the cursor is over
|
||||||
|
// the system UI. Android N native capture can only capture over views owned
|
||||||
|
// by the application.
|
||||||
|
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
|
return new ShieldCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Android N+ native mouse capture");
|
||||||
|
return new AndroidCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
|
return new EvdevCaptureProvider(activity, rootListener);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Mouse capture not available");
|
||||||
|
return new NullCaptureProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
public abstract class InputCaptureProvider {
|
||||||
|
public abstract void enableCapture();
|
||||||
|
public abstract void disableCapture();
|
||||||
|
public void destroy() {}
|
||||||
|
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
|
public class NullCaptureProvider extends InputCaptureProvider {
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.input.InputManager;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
||||||
|
// mode without having to grab the input device (which requires root). The data comes in the form
|
||||||
|
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
||||||
|
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
||||||
|
//
|
||||||
|
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
||||||
|
|
||||||
|
public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||||
|
private static boolean nvExtensionSupported;
|
||||||
|
private static Method methodSetCursorVisibility;
|
||||||
|
private static int AXIS_RELATIVE_X;
|
||||||
|
private static int AXIS_RELATIVE_Y;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
||||||
|
|
||||||
|
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
||||||
|
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
||||||
|
|
||||||
|
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
||||||
|
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
||||||
|
|
||||||
|
nvExtensionSupported = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
nvExtensionSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShieldCaptureProvider(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return nvExtensionSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setCursorVisibility(boolean visible) {
|
||||||
|
try {
|
||||||
|
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
setCursorVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
setCursorVisibility(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||||
|
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public abstract class AbstractController {
|
||||||
|
|
||||||
|
private final int deviceId;
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
|
||||||
|
protected short buttonFlags;
|
||||||
|
protected float leftTrigger, rightTrigger;
|
||||||
|
protected float rightStickX, rightStickY;
|
||||||
|
protected float leftStickX, leftStickY;
|
||||||
|
|
||||||
|
public int getControllerId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setButtonFlag(int buttonFlag, int data) {
|
||||||
|
if (data != 0) {
|
||||||
|
buttonFlags |= buttonFlag;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonFlags &= ~buttonFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void reportInput() {
|
||||||
|
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean start();
|
||||||
|
public abstract void stop();
|
||||||
|
|
||||||
|
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceRemoved() {
|
||||||
|
listener.deviceRemoved(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceAdded() {
|
||||||
|
listener.deviceAdded(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
import android.hardware.usb.UsbEndpoint;
|
||||||
|
import android.hardware.usb.UsbInterface;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public abstract class AbstractXboxController extends AbstractController {
|
||||||
|
protected final UsbDevice device;
|
||||||
|
protected final UsbDeviceConnection connection;
|
||||||
|
|
||||||
|
private Thread inputThread;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
protected UsbEndpoint inEndpt, outEndpt;
|
||||||
|
|
||||||
|
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(deviceId, listener);
|
||||||
|
this.device = device;
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread createInputThread() {
|
||||||
|
return new Thread() {
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted() && !stopped) {
|
||||||
|
byte[] buffer = new byte[64];
|
||||||
|
|
||||||
|
int res;
|
||||||
|
|
||||||
|
//
|
||||||
|
// There's no way that I can tell to determine if a device has failed
|
||||||
|
// or if the timeout has simply expired. We'll check how long the transfer
|
||||||
|
// took to fail and assume the device failed if it happened before the timeout
|
||||||
|
// expired.
|
||||||
|
//
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Read the next input state packet
|
||||||
|
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||||
|
|
||||||
|
// If we get a zero length response, treat it as an error
|
||||||
|
if (res == 0) {
|
||||||
|
res = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||||
|
LimeLog.warning("Detected device I/O error");
|
||||||
|
AbstractXboxController.this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (res == -1 && !isInterrupted() && !stopped);
|
||||||
|
|
||||||
|
if (res == -1 || stopped) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
||||||
|
// Report input if handleRead() returns true
|
||||||
|
reportInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean start() {
|
||||||
|
// Force claim all interfaces
|
||||||
|
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||||
|
UsbInterface iface = device.getInterface(i);
|
||||||
|
|
||||||
|
if (!connection.claimInterface(iface, true)) {
|
||||||
|
LimeLog.warning("Failed to claim interfaces");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
UsbInterface iface = device.getInterface(0);
|
||||||
|
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||||
|
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||||
|
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||||
|
if (inEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate IN endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inEndpt = endpt;
|
||||||
|
}
|
||||||
|
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||||
|
if (outEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate OUT endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outEndpt = endpt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (inEndpt == null || outEndpt == null) {
|
||||||
|
LimeLog.warning("Missing required endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the init function
|
||||||
|
if (!doInit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening for controller input
|
||||||
|
inputThread = createInputThread();
|
||||||
|
inputThread.start();
|
||||||
|
|
||||||
|
// Now report we're added
|
||||||
|
notifyDeviceAdded();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped = true;
|
||||||
|
|
||||||
|
// Stop the input thread
|
||||||
|
if (inputThread != null) {
|
||||||
|
inputThread.interrupt();
|
||||||
|
inputThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report the device removed
|
||||||
|
notifyDeviceRemoved();
|
||||||
|
|
||||||
|
// Close the USB connection
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||||
|
protected abstract boolean doInit();
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@ import android.hardware.usb.UsbDevice;
|
|||||||
import android.hardware.usb.UsbDeviceConnection;
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
@@ -26,10 +28,10 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||||
|
|
||||||
private final ArrayList<XboxOneController> controllers = new ArrayList<>();
|
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
private UsbDriverListener listener;
|
||||||
private static int nextDeviceId;
|
private int nextDeviceId;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
||||||
@@ -42,7 +44,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
@Override
|
@Override
|
||||||
public void deviceRemoved(int controllerId) {
|
public void deviceRemoved(int controllerId) {
|
||||||
// Remove the the controller from our list (if not removed already)
|
// Remove the the controller from our list (if not removed already)
|
||||||
for (XboxOneController controller : controllers) {
|
for (AbstractController controller : controllers) {
|
||||||
if (controller.getControllerId() == controllerId) {
|
if (controller.getControllerId() == controllerId) {
|
||||||
controllers.remove(controller);
|
controllers.remove(controller);
|
||||||
break;
|
break;
|
||||||
@@ -70,14 +72,14 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Initial attachment broadcast
|
// Initial attachment broadcast
|
||||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// Continue the state machine
|
// Continue the state machine
|
||||||
handleUsbDeviceState(device);
|
handleUsbDeviceState(device);
|
||||||
}
|
}
|
||||||
// Subsequent permission dialog completion intent
|
// Subsequent permission dialog completion intent
|
||||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// If we got this far, we've already found we're able to handle this device
|
// If we got this far, we've already found we're able to handle this device
|
||||||
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||||
@@ -93,7 +95,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Report all controllerMap that already exist
|
// Report all controllerMap that already exist
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
for (XboxOneController controller : controllers) {
|
for (AbstractController controller : controllers) {
|
||||||
listener.deviceAdded(controller.getControllerId());
|
listener.deviceAdded(controller.getControllerId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +104,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
private void handleUsbDeviceState(UsbDevice device) {
|
||||||
// Are we able to operate it?
|
// Are we able to operate it?
|
||||||
if (XboxOneController.canClaimDevice(device)) {
|
if (shouldClaimDevice(device)) {
|
||||||
// Do we have permission yet?
|
// Do we have permission yet?
|
||||||
if (!usbManager.hasPermission(device)) {
|
if (!usbManager.hasPermission(device)) {
|
||||||
// Let's ask for permission
|
// Let's ask for permission
|
||||||
@@ -117,8 +119,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to initialize it
|
|
||||||
XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
AbstractController controller;
|
||||||
|
|
||||||
|
if (XboxOneController.canClaimDevice(device)) {
|
||||||
|
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||||
|
}
|
||||||
|
else if (Xbox360Controller.canClaimDevice(device)) {
|
||||||
|
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Unreachable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the controller
|
||||||
if (!controller.start()) {
|
if (!controller.start()) {
|
||||||
connection.close();
|
connection.close();
|
||||||
return;
|
return;
|
||||||
@@ -129,6 +144,34 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isRecognizedInputDevice(UsbDevice device) {
|
||||||
|
// On KitKat and later, we can determine if this VID and PID combo
|
||||||
|
// matches an existing input device and defer to the built-in controller
|
||||||
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
|
InputDevice inputDev = InputDevice.getDevice(id);
|
||||||
|
|
||||||
|
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||||
|
inputDev.getProductId() == device.getProductId()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldClaimDevice(UsbDevice device) {
|
||||||
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
|
// if we know the kernel isn't already driving this device.
|
||||||
|
return XboxOneController.canClaimDevice(device) ||
|
||||||
|
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
@@ -141,7 +184,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Enumerate existing devices
|
// Enumerate existing devices
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
if (XboxOneController.canClaimDevice(dev)) {
|
if (shouldClaimDevice(dev)) {
|
||||||
// Start the process of claiming this device
|
// Start the process of claiming this device
|
||||||
handleUsbDeviceState(dev);
|
handleUsbDeviceState(dev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class Xbox360Controller extends AbstractXboxController {
|
||||||
|
private static final int XB360_IFACE_SUBCLASS = 93;
|
||||||
|
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x12ab, // Unknown
|
||||||
|
0x1430, // RedOctane
|
||||||
|
0x146b, // BigBen
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x1532, // Razer Sabertooth
|
||||||
|
0x15e4, // Numark
|
||||||
|
0x162e, // Joytech
|
||||||
|
};
|
||||||
|
|
||||||
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (device.getVendorId() == supportedVid &&
|
||||||
|
device.getInterfaceCount() >= 1 &&
|
||||||
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||||
|
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(device, connection, deviceId, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int unsignByte(byte b) {
|
||||||
|
if (b < 0) {
|
||||||
|
return b + 256;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
|
if (buffer.limit() < 14) {
|
||||||
|
LimeLog.severe("Read too small: "+buffer.limit());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip first short
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
|
||||||
|
// DPAD
|
||||||
|
byte b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
||||||
|
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
||||||
|
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
// Start/Select
|
||||||
|
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
||||||
|
|
||||||
|
// LS/RS
|
||||||
|
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
// ABXY buttons
|
||||||
|
b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||||
|
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
// LB/RB
|
||||||
|
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
// Xbox button
|
||||||
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
||||||
|
|
||||||
|
// Triggers
|
||||||
|
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||||
|
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||||
|
|
||||||
|
// Left stick
|
||||||
|
leftStickX = buffer.getShort() / 32767.0f;
|
||||||
|
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
// Right stick
|
||||||
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
// Return true to send input
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendLedCommand(byte command) {
|
||||||
|
byte[] commandBuffer = {0x01, 0x03, command};
|
||||||
|
|
||||||
|
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
||||||
|
if (res != commandBuffer.length) {
|
||||||
|
LimeLog.warning("LED set transfer failed: "+res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doInit() {
|
||||||
|
// Turn the LED on corresponding to our device ID
|
||||||
|
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
||||||
|
|
||||||
|
// No need to fail init if the LED command fails
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,60 +3,30 @@ package com.limelight.binding.input.driver;
|
|||||||
import android.hardware.usb.UsbConstants;
|
import android.hardware.usb.UsbConstants;
|
||||||
import android.hardware.usb.UsbDevice;
|
import android.hardware.usb.UsbDevice;
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
import android.hardware.usb.UsbEndpoint;
|
|
||||||
import android.hardware.usb.UsbInterface;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
|
||||||
|
|
||||||
public class XboxOneController {
|
public class XboxOneController extends AbstractXboxController {
|
||||||
private final UsbDevice device;
|
|
||||||
private final UsbDeviceConnection connection;
|
|
||||||
private final int deviceId;
|
|
||||||
|
|
||||||
private Thread inputThread;
|
|
||||||
private UsbDriverListener listener;
|
|
||||||
private boolean stopped;
|
|
||||||
|
|
||||||
private short buttonFlags;
|
|
||||||
private float leftTrigger, rightTrigger;
|
|
||||||
private float rightStickX, rightStickY;
|
|
||||||
private float leftStickX, leftStickY;
|
|
||||||
|
|
||||||
private static final int MICROSOFT_VID = 0x045e;
|
|
||||||
private static final int XB1_IFACE_SUBCLASS = 71;
|
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||||
private static final int XB1_IFACE_PROTOCOL = 208;
|
private static final int XB1_IFACE_PROTOCOL = 208;
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x24c6, // PowerA
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME: odata_serial
|
// FIXME: odata_serial
|
||||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
|
|
||||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
this.device = device;
|
super(device, connection, deviceId, listener);
|
||||||
this.connection = connection;
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getControllerId() {
|
|
||||||
return this.deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setButtonFlag(int buttonFlag, int data) {
|
|
||||||
if (data != 0) {
|
|
||||||
buttonFlags |= buttonFlag;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buttonFlags &= ~buttonFlag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportInput() {
|
|
||||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
|
||||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processButtons(ByteBuffer buffer) {
|
private void processButtons(ByteBuffer buffer) {
|
||||||
@@ -90,109 +60,42 @@ public class XboxOneController {
|
|||||||
|
|
||||||
rightStickX = buffer.getShort() / 32767.0f;
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
reportInput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processPacket(ByteBuffer buffer) {
|
@Override
|
||||||
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
switch (buffer.get())
|
switch (buffer.get())
|
||||||
{
|
{
|
||||||
case 0x20:
|
case 0x20:
|
||||||
buffer.position(buffer.position()+3);
|
buffer.position(buffer.position()+3);
|
||||||
processButtons(buffer);
|
processButtons(buffer);
|
||||||
break;
|
return true;
|
||||||
|
|
||||||
case 0x07:
|
case 0x07:
|
||||||
buffer.position(buffer.position() + 3);
|
buffer.position(buffer.position() + 3);
|
||||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||||
reportInput();
|
return true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startInputThread(final UsbEndpoint inEndpt) {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
inputThread = new Thread() {
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
public void run() {
|
if (device.getVendorId() == supportedVid &&
|
||||||
while (!isInterrupted() && !stopped) {
|
device.getInterfaceCount() >= 1 &&
|
||||||
byte[] buffer = new byte[64];
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||||
int res;
|
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
//
|
|
||||||
// There's no way that I can tell to determine if a device has failed
|
|
||||||
// or if the timeout has simply expired. We'll check how long the transfer
|
|
||||||
// took to fail and assume the device failed if it happened before the timeout
|
|
||||||
// expired.
|
|
||||||
//
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Read the next input state packet
|
|
||||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
|
||||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
|
||||||
|
|
||||||
// If we get a zero length response, treat it as an error
|
|
||||||
if (res == 0) {
|
|
||||||
res = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
|
||||||
LimeLog.warning("Detected device I/O error");
|
|
||||||
XboxOneController.this.stop();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (res == -1 && !isInterrupted() && !stopped);
|
|
||||||
|
|
||||||
if (res == -1 || stopped) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
inputThread.setName("Xbox One Controller - Input Thread");
|
|
||||||
inputThread.start();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean start() {
|
@Override
|
||||||
// Force claim all interfaces
|
protected boolean doInit() {
|
||||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
|
||||||
UsbInterface iface = device.getInterface(i);
|
|
||||||
|
|
||||||
if (!connection.claimInterface(iface, true)) {
|
|
||||||
LimeLog.warning("Failed to claim interfaces");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the endpoints
|
|
||||||
UsbEndpoint outEndpt = null;
|
|
||||||
UsbEndpoint inEndpt = null;
|
|
||||||
UsbInterface iface = device.getInterface(0);
|
|
||||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
|
||||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
|
||||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
|
||||||
if (inEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate IN endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
inEndpt = endpt;
|
|
||||||
}
|
|
||||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
|
||||||
if (outEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate OUT endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
outEndpt = endpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the required endpoints were present
|
|
||||||
if (inEndpt == null || outEndpt == null) {
|
|
||||||
LimeLog.warning("Missing required endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the initialization packet
|
// Send the initialization packet
|
||||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||||
if (res != XB1_INIT_DATA.length) {
|
if (res != XB1_INIT_DATA.length) {
|
||||||
@@ -200,40 +103,6 @@ public class XboxOneController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start listening for controller input
|
|
||||||
startInputThread(inEndpt);
|
|
||||||
|
|
||||||
// Report this device added via the listener
|
|
||||||
listener.deviceAdded(deviceId);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopped = true;
|
|
||||||
|
|
||||||
// Stop the input thread
|
|
||||||
if (inputThread != null) {
|
|
||||||
inputThread.interrupt();
|
|
||||||
inputThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report the device removed
|
|
||||||
listener.deviceRemoved(deviceId);
|
|
||||||
|
|
||||||
// Close the USB connection
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
|
||||||
return device.getVendorId() == MICROSOFT_VID &&
|
|
||||||
device.getInterfaceCount() >= 1 &&
|
|
||||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
|
||||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
|
||||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-17
@@ -1,8 +1,11 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.app.Activity;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -12,7 +15,7 @@ import java.io.OutputStream;
|
|||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
public class EvdevHandler {
|
public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||||
|
|
||||||
private final EvdevListener listener;
|
private final EvdevListener listener;
|
||||||
private final String libraryPath;
|
private final String libraryPath;
|
||||||
@@ -23,6 +26,8 @@ public class EvdevHandler {
|
|||||||
private Process su;
|
private Process su;
|
||||||
private ServerSocket servSock;
|
private ServerSocket servSock;
|
||||||
private Socket evdevSock;
|
private Socket evdevSock;
|
||||||
|
private Activity activity;
|
||||||
|
private boolean started = false;
|
||||||
|
|
||||||
private static final byte UNGRAB_REQUEST = 1;
|
private static final byte UNGRAB_REQUEST = 1;
|
||||||
private static final byte REGRAB_REQUEST = 2;
|
private static final byte REGRAB_REQUEST = 2;
|
||||||
@@ -49,6 +54,7 @@ public class EvdevHandler {
|
|||||||
try {
|
try {
|
||||||
su = builder.start();
|
su = builder.start();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -58,6 +64,7 @@ public class EvdevHandler {
|
|||||||
try {
|
try {
|
||||||
suOut.writeChars(libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort()+"\n");
|
suOut.writeChars(libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort()+"\n");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -155,23 +162,48 @@ public class EvdevHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public EvdevHandler(Context context, EvdevListener listener) {
|
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.libraryPath = context.getApplicationInfo().nativeLibraryDir;
|
this.activity = activity;
|
||||||
|
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void regrabAll() {
|
public static boolean isCaptureProviderSupported() {
|
||||||
if (!shutdown && evdevOut != null) {
|
return LimelightBuildProps.ROOT_BUILD;
|
||||||
try {
|
}
|
||||||
evdevOut.write(REGRAB_REQUEST);
|
|
||||||
} catch (IOException e) {
|
private void reportDeviceNotRooted() {
|
||||||
e.printStackTrace();
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(activity, "This device is not rooted - Mouse capture is unavailable", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
if (!started) {
|
||||||
|
// Start the handler thread if it's our first time
|
||||||
|
// capturing
|
||||||
|
handlerThread.start();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Send a request to regrab if we're already capturing
|
||||||
|
if (!shutdown && evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.write(REGRAB_REQUEST);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ungrabAll() {
|
@Override
|
||||||
if (!shutdown && evdevOut != null) {
|
public void disableCapture() {
|
||||||
|
if (started && !shutdown && evdevOut != null) {
|
||||||
try {
|
try {
|
||||||
evdevOut.write(UNGRAB_REQUEST);
|
evdevOut.write(UNGRAB_REQUEST);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -180,15 +212,16 @@ public class EvdevHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
@Override
|
||||||
handlerThread.start();
|
public void destroy() {
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
// We need to stop the process in this context otherwise
|
// We need to stop the process in this context otherwise
|
||||||
// we could get stuck waiting on output from the process
|
// we could get stuck waiting on output from the process
|
||||||
// in order to terminate it.
|
// in order to terminate it.
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
shutdown = true;
|
shutdown = true;
|
||||||
handlerThread.interrupt();
|
handlerThread.interrupt();
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ public class DigitalButton extends VirtualControllerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DigitalButtonListener> listeners = new ArrayList<DigitalButtonListener>();
|
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
||||||
private String text = "";
|
private String text = "";
|
||||||
private int icon = -1;
|
private int icon = -1;
|
||||||
private long timerLongClickTimeout = 3000;
|
private long timerLongClickTimeout = 3000;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class DigitalPad extends VirtualControllerElement {
|
|||||||
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
||||||
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
||||||
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
||||||
List<DigitalPadListener> listeners = new ArrayList<DigitalPadListener>();
|
List<DigitalPadListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
private static final int DPAD_MARGIN = 5;
|
private static final int DPAD_MARGIN = 5;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ public class VirtualController {
|
|||||||
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
||||||
private Button buttonConfigure = null;
|
private Button buttonConfigure = null;
|
||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<VirtualControllerElement>();
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
|
|
||||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||||
this.connection = conn;
|
this.connection = conn;
|
||||||
|
|||||||
@@ -475,8 +475,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException ignored) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the decoder
|
// We could stop the decoder here, but it seems to cause some problems
|
||||||
videoDecoder.stop();
|
// so we'll just let release take care of it.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class MediaCodecHelper {
|
|||||||
private static final List<String> whitelistedHevcDecoders;
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
directSubmitPrefixes = new LinkedList<String>();
|
directSubmitPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// These decoders have low enough input buffer latency that they
|
// These decoders have low enough input buffer latency that they
|
||||||
// can be directly invoked from the receive thread
|
// can be directly invoked from the receive thread
|
||||||
@@ -48,33 +48,38 @@ public class MediaCodecHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
preferredDecoders = new LinkedList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// Software decoders that don't support H264 high profile
|
// Software decoders that don't support H264 high profile
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
|
|
||||||
|
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||||
|
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||||
|
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||||
|
|
||||||
baselineProfileHackPrefixes = new LinkedList<String>();
|
baselineProfileHackPrefixes = new LinkedList<>();
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<String>();
|
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +218,7 @@ public class MediaCodecHelper {
|
|||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
@@ -371,7 +376,7 @@ public class MediaCodecHelper {
|
|||||||
break;
|
break;
|
||||||
cpuInfo.append((char)ch);
|
cpuInfo.append((char)ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cpuInfo.toString();
|
return cpuInfo.toString();
|
||||||
} finally {
|
} finally {
|
||||||
br.close();
|
br.close();
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
public List<ComputerDetails> getAllComputers() {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 500;
|
private static final int FAST_POLL_TIMEOUT = 500;
|
||||||
private static final int OFFLINE_POLL_TRIES = 5;
|
private static final int OFFLINE_POLL_TRIES = 5;
|
||||||
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
|
|
||||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
private IdentityManager idManager;
|
private IdentityManager idManager;
|
||||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
|
||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
private boolean pollingActive = false;
|
private boolean pollingActive = false;
|
||||||
@@ -232,8 +233,10 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onUnbind(Intent intent) {
|
public boolean onUnbind(Intent intent) {
|
||||||
// Stop mDNS autodiscovery
|
if (discoveryBinder != null) {
|
||||||
discoveryBinder.stopDiscovery();
|
// Stop mDNS autodiscovery
|
||||||
|
discoveryBinder.stopDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
pollingActive = false;
|
pollingActive = false;
|
||||||
@@ -661,6 +664,7 @@ public class ComputerManagerService extends Service {
|
|||||||
thread = new Thread() {
|
thread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
int emptyAppListResponses = 0;
|
||||||
do {
|
do {
|
||||||
InetAddress selectedAddr;
|
InetAddress selectedAddr;
|
||||||
|
|
||||||
@@ -705,7 +709,15 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
|
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||||
|
|
||||||
|
// The app list might actually be empty, so if we get an empty response a few times
|
||||||
|
// in a row, we'll go ahead and believe it.
|
||||||
|
emptyAppListResponses++;
|
||||||
|
}
|
||||||
|
if (appList != null && !appList.isEmpty() &&
|
||||||
|
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
OutputStream cacheOut = null;
|
OutputStream cacheOut = null;
|
||||||
try {
|
try {
|
||||||
@@ -721,6 +733,11 @@ public class ComputerManagerService extends Service {
|
|||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset empty count if it wasn't empty this time
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
emptyAppListResponses = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the computer
|
// Update the computer
|
||||||
computer.rawAppList = appList;
|
computer.rawAppList = appList;
|
||||||
receivedAppList = true;
|
receivedAppList = true;
|
||||||
@@ -731,8 +748,8 @@ public class ComputerManagerService extends Service {
|
|||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (appList == null || appList.isEmpty()) {
|
||||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import com.limelight.grid.assets.MemoryAssetLoader;
|
|||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
protected final Context context;
|
protected final Context context;
|
||||||
protected final int defaultImageRes;
|
protected final int defaultImageRes;
|
||||||
protected final int layoutId;
|
protected final int layoutId;
|
||||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
protected final ArrayList<T> itemList = new ArrayList<>();
|
||||||
protected final LayoutInflater inflater;
|
protected final LayoutInflater inflater;
|
||||||
|
|
||||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ public class CachedAppAssetLoader {
|
|||||||
private LoaderTuple tuple;
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
||||||
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
this.imageViewRef = new WeakReference<>(imageView);
|
||||||
this.diskOnly = diskOnly;
|
this.diskOnly = diskOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ public class CachedAppAssetLoader {
|
|||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
LoaderTask loaderTask) {
|
LoaderTask loaderTask) {
|
||||||
super(res, bitmap);
|
super(res, bitmap);
|
||||||
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
loaderTaskReference = new WeakReference<>(loaderTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoaderTask getLoaderTask() {
|
public LoaderTask getLoaderTask() {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.limelight.LimeLog;
|
|||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import android.widget.Toast;
|
|||||||
public class AddComputerManually extends Activity {
|
public class AddComputerManually extends Activity {
|
||||||
private TextView hostText;
|
private TextView hostText;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
||||||
private Thread addThread;
|
private Thread addThread;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.limelight.preferences;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use small mode on anything smaller than a 7" tablet
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
|
||||||
|
public class StreamView extends SurfaceView {
|
||||||
|
private double desiredAspectRatio;
|
||||||
|
|
||||||
|
public void setDesiredAspectRatio(double aspectRatio) {
|
||||||
|
this.desiredAspectRatio = aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(21)
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||||
|
if (desiredAspectRatio == 0) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||||
|
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
|
||||||
|
int measuredHeight, measuredWidth;
|
||||||
|
if (widthSize > heightSize * desiredAspectRatio) {
|
||||||
|
measuredHeight = heightSize;
|
||||||
|
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||||
|
} else {
|
||||||
|
measuredWidth = widthSize;
|
||||||
|
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ public class Dialog implements Runnable {
|
|||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
private ProgressDialog progress;
|
private ProgressDialog progress;
|
||||||
private final boolean finish;
|
private final boolean finish;
|
||||||
|
|
||||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Android.mk for Moonlight's ENet JNI binding
|
||||||
|
MY_LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
|
include $(call all-subdir-makefiles)
|
||||||
|
|
||||||
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE := jnienet
|
||||||
|
|
||||||
|
LOCAL_SRC_FILES := jnienet.c \
|
||||||
|
enet/callbacks.c \
|
||||||
|
enet/compress.c \
|
||||||
|
enet/host.c \
|
||||||
|
enet/list.c \
|
||||||
|
enet/packet.c \
|
||||||
|
enet/peer.c \
|
||||||
|
enet/protocol.c \
|
||||||
|
enet/unix.c \
|
||||||
|
enet/win32.c \
|
||||||
|
|
||||||
|
LOCAL_CFLAGS := -DHAS_SOCKLEN_T=1
|
||||||
|
LOCAL_C_INCLUDES := $(LOCAL_PATH)/enet/include
|
||||||
|
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
Submodule
+1
Submodule app/src/main/jni/jnienet/enet added at 7546b505c1
@@ -0,0 +1,148 @@
|
|||||||
|
#include "enet/enet.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#define CLIENT_TO_LONG(x) ((intptr_t)(x))
|
||||||
|
#define LONG_TO_CLIENT(x) ((ENetHost*)(intptr_t)(x))
|
||||||
|
|
||||||
|
#define PEER_TO_LONG(x) ((intptr_t)(x))
|
||||||
|
#define LONG_TO_PEER(x) ((ENetPeer*)(intptr_t)(x))
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_initializeEnet(JNIEnv *env, jobject class) {
|
||||||
|
return enet_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_createClient(JNIEnv *env, jobject class, jstring address) {
|
||||||
|
ENetAddress enetAddress;
|
||||||
|
const char *addrStr;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
// Perform a lookup on the address to determine the address family
|
||||||
|
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||||
|
err = enet_address_set_host(&enetAddress, addrStr);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||||
|
if (err < 0) {
|
||||||
|
return CLIENT_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client that can use 1 outgoing connection and 1 channel
|
||||||
|
return CLIENT_TO_LONG(enet_host_create(enetAddress.address.ss_family, NULL, 1, 1, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_connectToPeer(JNIEnv *env, jobject class, jlong client, jstring address, jint port, jint timeout) {
|
||||||
|
ENetPeer* peer;
|
||||||
|
ENetAddress enetAddress;
|
||||||
|
ENetEvent event;
|
||||||
|
const char *addrStr;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
// Initialize the ENet address
|
||||||
|
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||||
|
err = enet_address_set_host(&enetAddress, addrStr);
|
||||||
|
enet_address_set_port(&enetAddress, port);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||||
|
if (err < 0) {
|
||||||
|
return PEER_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the connection
|
||||||
|
peer = enet_host_connect(LONG_TO_CLIENT(client), &enetAddress, 1, 0);
|
||||||
|
if (peer == NULL) {
|
||||||
|
return PEER_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the connect to complete
|
||||||
|
if (enet_host_service(LONG_TO_CLIENT(client), &event, timeout) <= 0 || event.type != ENET_EVENT_TYPE_CONNECT) {
|
||||||
|
enet_peer_reset(peer);
|
||||||
|
return PEER_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the connect verify ACK is sent immediately
|
||||||
|
enet_host_flush(LONG_TO_CLIENT(client));
|
||||||
|
|
||||||
|
// Set the max peer timeout to 10 seconds
|
||||||
|
enet_peer_timeout(peer, ENET_PEER_TIMEOUT_LIMIT, ENET_PEER_TIMEOUT_MINIMUM, 10000);
|
||||||
|
|
||||||
|
return PEER_TO_LONG(peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_readPacket(JNIEnv *env, jobject class, jlong client, jbyteArray data, jint length, jint timeout) {
|
||||||
|
jint err;
|
||||||
|
jbyte* dataPtr;
|
||||||
|
ENetEvent event;
|
||||||
|
|
||||||
|
// Wait for a receive event, timeout, or disconnect
|
||||||
|
err = enet_host_service(LONG_TO_CLIENT(client), &event, timeout);
|
||||||
|
if (err <= 0) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
else if (event.type != ENET_EVENT_TYPE_RECEIVE) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the packet isn't too large
|
||||||
|
if (event.packet->dataLength > length) {
|
||||||
|
enet_packet_destroy(event.packet);
|
||||||
|
return event.packet->dataLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the packet data into the caller's buffer
|
||||||
|
dataPtr = (*env)->GetByteArrayElements(env, data, 0);
|
||||||
|
memcpy(dataPtr, event.packet->data, event.packet->dataLength);
|
||||||
|
err = event.packet->dataLength;
|
||||||
|
(*env)->ReleaseByteArrayElements(env, data, dataPtr, 0);
|
||||||
|
|
||||||
|
// Free the packet
|
||||||
|
enet_packet_destroy(event.packet);
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_writePacket(JNIEnv *env, jobject class, jlong client, jlong peer, jbyteArray data, jint length, jint packetFlags) {
|
||||||
|
ENetPacket* packet;
|
||||||
|
jboolean ret;
|
||||||
|
jbyte* dataPtr;
|
||||||
|
|
||||||
|
dataPtr = (*env)->GetByteArrayElements(env, data, 0);
|
||||||
|
|
||||||
|
// Create the reliable packet that describes our outgoing message
|
||||||
|
packet = enet_packet_create(dataPtr, length, packetFlags);
|
||||||
|
if (packet != NULL) {
|
||||||
|
// Send the message to the peer
|
||||||
|
if (enet_peer_send(LONG_TO_PEER(peer), 0, packet) < 0) {
|
||||||
|
// This can fail if the peer has been disconnected
|
||||||
|
enet_packet_destroy(packet);
|
||||||
|
ret = JNI_FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Force the client to send the packet now
|
||||||
|
enet_host_flush(LONG_TO_CLIENT(client));
|
||||||
|
ret = JNI_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret = JNI_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->ReleaseByteArrayElements(env, data, dataPtr, JNI_ABORT);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_destroyClient(JNIEnv *env, jobject class, jlong client) {
|
||||||
|
enet_host_destroy(LONG_TO_CLIENT(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_disconnectPeer(JNIEnv *env, jobject class, jlong peer) {
|
||||||
|
enet_peer_disconnect_now(LONG_TO_PEER(peer), 0);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".Game" >
|
tools:context=".Game" >
|
||||||
|
|
||||||
<SurfaceView
|
<com.limelight.ui.StreamView
|
||||||
android:id="@+id/surfaceView"
|
android:id="@+id/surfaceView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="video_format_names">
|
<string-array name="video_format_names">
|
||||||
<item>Use H.265 only if safe</item>
|
<item>Use H.265 only if stable</item>
|
||||||
<item>Always use H.265 if available</item>
|
<item>Always use H.265 (may crash)</item>
|
||||||
<item>Never use H.265</item>
|
<item>Never use H.265</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="video_format_values" translatable="false">
|
<string-array name="video_format_values" translatable="false">
|
||||||
|
|||||||
@@ -103,8 +103,8 @@
|
|||||||
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
|
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
|
||||||
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
|
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
|
||||||
<string name="suffix_seekbar_deadzone">%</string>
|
<string name="suffix_seekbar_deadzone">%</string>
|
||||||
<string name="title_checkbox_xb1_driver">Xbox One controller driver</string>
|
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
|
||||||
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox One controller support.</string>
|
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox controller support.</string>
|
||||||
|
|
||||||
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
|
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
|
||||||
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
|
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:1.3.0'
|
classpath 'com.android.tools.build:gradle:2.1.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Settings specified in this file will override any Gradle settings
|
||||||
|
# configured through the IDE.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||||
|
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
org.gradle.jvmargs=-Xmx3072m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
#Sun Dec 07 22:52:07 PST 2014
|
#Sat Feb 06 16:21:20 EST 2016
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user