Compare commits

..
2 Commits
Author SHA1 Message Date
Cameron Gutman 2ba3c0e477 Update common again 2014-11-11 01:09:45 -08:00
Cameron Gutman fe27957000 Update 2.9 series to support GFE 2.1.4 2014-11-10 22:09:56 -08:00
508 changed files with 101030 additions and 12449 deletions
-3
View File
@@ -1,3 +0,0 @@
[submodule "app/src/main/jni/jnienet/enet"]
path = app/src/main/jni/jnienet/enet
url = https://github.com/cgutman/enet.git
+9 -15
View File
@@ -1,14 +1,12 @@
#Moonlight
#Limelight
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
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.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
##Features
@@ -18,8 +16,8 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for mo
##Installation
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* Download and install Limelight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
##Requirements
@@ -33,7 +31,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for mo
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Moonlight and tap on your PC in the list
* When on the same network as your PC, open Limelight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* Tap your PC again to view the list of apps to stream
* Play games!
@@ -46,11 +44,7 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code
3. Send Pull Requests
##Building
* Install Android Studio and the Android NDK
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors
@@ -59,5 +53,5 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
* [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername)
Moonlight is the work of students at [Case Western](http://case.edu) and was
Limelight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org).
+19 -25
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
@@ -9,15 +9,11 @@
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateNonRootDebugAndroidTestSources</task>
<task>generateNonRootDebugSources</task>
</afterSyncTasks>
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -26,9 +22,8 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -36,7 +31,6 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
@@ -44,12 +38,11 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -106,15 +99,16 @@
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.1.0-RC1" level="project" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9-patched" level="project" />
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
<orderEntry type="library" exported="" name="okio-1.0.1" level="project" />
</component>
</module>
</module>
+12 -25
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.1"
compileSdkVersion 21
buildToolsVersion "21.0.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 24
targetSdkVersion 21
versionName "4.7"
versionCode = 104
versionName "2.9.0.1"
versionCode = 40
}
productFlavors {
@@ -25,24 +25,13 @@ android {
}
}
lintOptions {
disable 'MissingTranslation'
}
buildTypes {
release {
minifyEnabled false
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
// 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 = []
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
@@ -73,14 +62,12 @@ android {
}
dependencies {
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
compile files('libs/jmdns-3.4.2.jar')
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1'
compile group: 'com.squareup.okio', name:'okio', version:'1.0.1'
compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar')
compile files('libs/jcodec-0.1.9-patched.jar')
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15 -15
View File
@@ -11,30 +11,35 @@
<uses-feature android:name="android.hardware.touchscreen" 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.usb.host" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/atv_banner"
android:theme="@style/AppTheme" >
<!-- Samsung multi-window support -->
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<!-- Launcher for traditional devices -->
<activity
android:name=".PcView"
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.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" />
</intent-filter>
</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
android:name=".AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
@@ -59,22 +64,17 @@
<activity
android:name=".Game"
android:screenOrientation="sensorLandscape"
android:theme="@style/StreamTheme"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" />
android:value="com.limelight.Connection" />
</activity>
<service
android:name=".discovery.DiscoveryService"
android:label="mDNS PC Auto-Discovery Service" />
<service
android:name=".computers.ComputerManagerService"
android:label="Computer Management Service" />
<service
android:name=".binding.input.driver.UsbDriverService"
android:label="Usb Driver Service" />
</application>
</manifest>
+252 -479
View File
@@ -1,522 +1,295 @@
package com.limelight;
import java.io.StringReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter;
private String uuidString;
private ComputerDetails computer;
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private boolean inForeground;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4;
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
runOnUiThread(new Runnable() {
@Override
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();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private void startComputerUpdates() {
// Don't start polling if we're not bound or in the foreground
if (managerBinder == null || !inForeground) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
}
private void populateAppGridWithCache() {
try {
// Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
}
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
}
private void loadAppsBlocking() {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
inForeground = true;
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
inForeground = false;
stopComputerUpdates();
}
private int getRunningAppId() {
public class AppView extends Activity {
private ListView appList;
private ArrayAdapter<AppObject> appListAdapter;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private final static int RESUME_ID = 1;
private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3;
public final static String ADDRESS_EXTRA = "Address";
public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_view);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
return;
}
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
try {
ipAddress = InetAddress.getByAddress(address);
} catch (UnknownHostException e) {
return;
}
// Setup the list view
appList = (ListView)findViewById(R.id.pcListView);
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
appListAdapter.setNotifyOnChange(false);
appList.setAdapter(appListAdapter);
appList.setItemsCanFocus(true);
appList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
}
else {
doStart(app.app);
}
}
});
registerForContextMenu(appList);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
}
@Override
protected void onResume() {
super.onResume();
updateAppList();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = appListAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
AppObject selectedApp = appListAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
}
else {
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
}
}
}
@Override
public void onContextMenuClosed(Menu menu) {
}
@Override
public void onContextMenuClosed(Menu menu) {
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case START_WTIH_QUIT:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
return true;
case QUIT_ID:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
}
}, null);
return true;
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
AppObject app = appListAdapter.getItem(info.position);
switch (item.getItemId())
{
case RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
return true;
case QUIT_ID:
doQuit(app.app);
return true;
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
}
}
private void updateUiWithServerinfo(final ComputerDetails details) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// Look through our current app list to tag the running app
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
updated = true;
}
else if (existingApp.app.getIsRunning()) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
updated = true;
}
else {
// This app wasn't running and still isn't
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
private static String generateString(NvApp app) {
StringBuilder str = new StringBuilder();
str.append(app.getAppName());
if (app.getIsRunning()) {
str.append(" - Running");
}
return str.toString();
}
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) {
boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunning(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
}
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
// Next handle app removals
int i = 0;
while (i < appGridAdapter.getCount()) {
boolean foundExistingApp = false;
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// Check if this app is in the latest list
for (NvApp app : appList) {
if (existingApp.app.getAppId() == app.getAppId()) {
foundExistingApp = true;
break;
}
}
// This app was removed in the latest app list
if (!foundExistingApp) {
appGridAdapter.removeApp(existingApp);
updated = true;
// Check this same index again because the item at i+1 is now at i after
// the removal
continue;
}
// Move on to the next item
i++;
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
private void addListPlaceholder() {
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
}
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
private void updateAppList() {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
appListAdapter.clear();
if (appList.isEmpty()) {
addListPlaceholder();
}
else {
for (NvApp app : appList) {
appListAdapter.add(new AppObject(generateString(app), app));
}
}
appListAdapter.notifyDataSetChanged();
}
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
}
}.start();
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(appGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject {
public final NvApp app;
public AppObject(NvApp app) {
if (app == null) {
throw new IllegalArgumentException("app must not be null");
}
this.app = app;
}
@Override
public String toString() {
return app.getAppName();
}
}
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = "Successfully quit "+app.getAppName();
}
else {
message = "Failed to quit "+app.getAppName();
}
updateAppList();
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
public class AppObject {
public String text;
public NvApp app;
public AppObject(String text, NvApp app) {
this.text = text;
this.app = app;
}
@Override
public String toString() {
return text;
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
package com.limelight;
/* This is a dummy class to allow for a separate icon
* and launcher for TV.
*/
public class PcViewTv extends PcView {}
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.http.LimelightCryptoProvider;
public class PlatformBinding {
public static String getDeviceName() {
String deviceName = android.os.Build.MODEL;
public static String getDeviceName() {
String deviceName = android.os.Build.MODEL;
deviceName = deviceName.replace(" ", "");
return deviceName;
}
public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer();
}
public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c);
}
}
public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer();
}
public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c);
}
}
@@ -9,94 +9,62 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
private AudioTrack track;
public static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override
public boolean streamInitialized(int channelCount, int channelMask, int samplesPerFrame, int sampleRate) {
int channelConfig;
int bufferSize;
int bytesPerFrame = (samplesPerFrame * 2);
@Override
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
switch (channelCount)
{
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
switch (channelCount)
{
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
// We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = bytesPerFrame * 2;
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
LimeLog.info("Audio track buffer size: "+bufferSize);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
track.play();
return true;
}
// Now try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2);
@Override
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
@Override
public void streamClosing() {
if (track != null) {
track.release();
}
}
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
}
LimeLog.info("Audio track buffer size: "+bufferSize);
return true;
}
@Override
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
@Override
public void streamClosing() {
if (track != null) {
track.release();
}
}
@Override
public int getCapabilities() {
return 0;
}
@Override
public int getCapabilities() {
return 0;
}
}
@@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider {
private final File certFile;
private final File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
}
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key");
}
private byte[] loadFileToBytes(File f) {
if (!f.exists()) {
return null;
}
try {
FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) {
private File certFile;
private File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
}
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key");
}
private byte[] loadFileToBytes(File f) {
if (!f.exists()) {
return null;
}
try {
FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) {
// Failed to read
fileData = null;
}
fin.close();
return fileData;
} catch (IOException e) {
return null;
}
}
fin.close();
return fileData;
} catch (IOException e) {
return null;
}
}
private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one");
return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile);
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
// If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one");
return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
@Override
public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
@Override
public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
}
File diff suppressed because it is too large Load Diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/**
* GFE's prefix for every key code
*/
private static final short KEY_PREFIX = (short) 0x80;
public static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
@@ -23,8 +23,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
@@ -1,241 +1,93 @@
package com.limelight.binding.input;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private boolean cancelled;
private boolean confirmedMove;
private boolean confirmedDrag;
private Timer dragTimer;
private double distanceMoved;
private double xFactor, yFactor;
private final NvConnection conn;
private final int actionIndex;
private final int referenceWidth;
private final int referenceHeight;
private final View targetView;
private static final int TAP_MOVEMENT_THRESHOLD = 20;
private static final int TAP_DISTANCE_THRESHOLD = 25;
private static final int TAP_TIME_THRESHOLD = 250;
private static final int DRAG_TIME_THRESHOLD = 650;
public TouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.referenceWidth = referenceWidth;
this.referenceHeight = referenceHeight;
this.targetView = view;
}
public int getActionIndex()
{
return actionIndex;
}
private boolean isWithinTapBounds(int touchX, int touchY)
{
int xDelta = Math.abs(touchX - originalTouchX);
int yDelta = Math.abs(touchY - originalTouchY);
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD;
}
private boolean isTap()
{
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
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;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
cancelled = confirmedDrag = confirmedMove = false;
distanceMoved = 0;
if (actionIndex == 0) {
// Start the timer for engaging a drag
startDragTimer();
}
return true;
}
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
// Cancel the drag timer
cancelDragTimer();
byte buttonIndex = getMouseButtonIndex();
if (confirmedDrag) {
// Raise the button after a drag
conn.sendMouseButtonUp(buttonIndex);
}
else if (isTap())
{
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
private synchronized void startDragTimer() {
dragTimer = new Timer(true);
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (TouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
}
// Check if someone cancelled us
if (dragTimer == null) {
return;
}
// Uncancellable now
dragTimer = null;
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
}
}, DRAG_TIME_THRESHOLD);
}
private synchronized void cancelDragTimer() {
if (dragTimer != null) {
dragTimer.cancel();
dragTimer = null;
}
}
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
// If we've already confirmed something, get out now
if (confirmedMove || confirmedDrag) {
return;
}
// If it leaves the tap bounds before the drag time expires, it's a move.
if (!isWithinTapBounds(eventX, eventY)) {
confirmedMove = true;
cancelDragTimer();
return;
}
// Check if we've exceeded the maximum distance moved
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
confirmedMove = true;
cancelDragTimer();
return;
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves and drags for the primary touch point
if (actionIndex == 0) {
checkForConfirmedMove(eventX, eventY);
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
// Fix up the signs
if (eventX < lastTouchX) {
deltaX = -deltaX;
}
if (eventY < lastTouchY) {
deltaY = -deltaY;
}
// If the scaling factor ended up rounding deltas to zero, wait until they are
// non-zero to update lastTouch that way devices that report small touch events often
// will work correctly
if (deltaX != 0) {
lastTouchX = eventX;
}
if (deltaY != 0) {
lastTouchY = eventY;
}
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
else {
lastTouchX = eventX;
lastTouchY = eventY;
}
}
return true;
}
public void cancelTouch() {
cancelled = true;
// Cancel the drag timer
cancelDragTimer();
// If it was a confirmed drag, we'll need to raise the button now
if (confirmedDrag) {
conn.sendMouseButtonUp(getMouseButtonIndex());
}
}
public boolean isCancelled() {
return cancelled;
}
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private NvConnection conn;
private int actionIndex;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex)
{
this.conn = conn;
this.actionIndex = actionIndex;
}
private boolean isTap()
{
int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
public boolean touchDownEvent(int eventX, int eventY)
{
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
return true;
}
public void touchUpEvent(int eventX, int eventY)
{
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
conn.sendMouseMove((short)(eventX - lastTouchX),
(short)(eventY - lastTouchY));
}
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}
return false;
}
}
@@ -1,59 +0,0 @@
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);
}
}
@@ -1,31 +0,0 @@
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 (EvdevCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
}
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android N+ native mouse capture");
return new AndroidCaptureProvider(activity);
}
else {
LimeLog.info("Mouse capture not available");
return new NullCaptureProvider();
}
}
}
@@ -1,21 +0,0 @@
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;
}
}
@@ -1,10 +0,0 @@
package com.limelight.binding.input.capture;
public class NullCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {}
@Override
public void disableCapture() {}
}
@@ -1,89 +0,0 @@
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);
}
}
@@ -1,47 +0,0 @@
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);
}
}
@@ -1,149 +0,0 @@
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();
}
@@ -1,11 +0,0 @@
package com.limelight.binding.input.driver;
public interface UsbDriverListener {
void reportControllerState(int controllerId, short buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger);
void deviceRemoved(int controllerId);
void deviceAdded(int controllerId);
}
@@ -1,213 +0,0 @@
package com.limelight.binding.input.driver;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.view.InputDevice;
import com.limelight.LimeLog;
import java.util.ArrayList;
public class UsbDriverService extends Service implements UsbDriverListener {
private static final String ACTION_USB_PERMISSION =
"com.limelight.USB_PERMISSION";
private UsbManager usbManager;
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
private final ArrayList<AbstractController> controllers = new ArrayList<>();
private UsbDriverListener listener;
private int nextDeviceId;
@Override
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
// Call through to the client's listener
if (listener != null) {
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
}
}
@Override
public void deviceRemoved(int controllerId) {
// Remove the the controller from our list (if not removed already)
for (AbstractController controller : controllers) {
if (controller.getControllerId() == controllerId) {
controllers.remove(controller);
break;
}
}
// Call through to the client's listener
if (listener != null) {
listener.deviceRemoved(controllerId);
}
}
@Override
public void deviceAdded(int controllerId) {
// Call through to the client's listener
if (listener != null) {
listener.deviceAdded(controllerId);
}
}
public class UsbEventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// Continue the state machine
handleUsbDeviceState(device);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_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)) {
handleUsbDeviceState(device);
}
}
}
}
public class UsbDriverBinder extends Binder {
public void setListener(UsbDriverListener listener) {
UsbDriverService.this.listener = listener;
// Report all controllerMap that already exist
if (listener != null) {
for (AbstractController controller : controllers) {
listener.deviceAdded(controller.getControllerId());
}
}
}
}
private void handleUsbDeviceState(UsbDevice device) {
// Are we able to operate it?
if (shouldClaimDevice(device)) {
// Do we have permission yet?
if (!usbManager.hasPermission(device)) {
// Let's ask for permission
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
return;
}
// Open the device
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection == null) {
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
return;
}
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()) {
connection.close();
return;
}
// Add this controller to the list
controllers.add(controller);
}
}
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
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
// Register for USB attach broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(receiver, filter);
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (shouldClaimDevice(dev)) {
// Start the process of claiming this device
handleUsbDeviceState(dev);
}
}
}
@Override
public void onDestroy() {
// Stop the attachment receiver
unregisterReceiver(receiver);
// Remove listeners
listener = null;
// Stop all controllers
while (controllers.size() > 0) {
// Stop and remove the controller
controllers.remove(0).stop();
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
@@ -1,136 +0,0 @@
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;
}
}
@@ -1,122 +0,0 @@
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 XboxOneController extends AbstractXboxController {
private static final int XB1_IFACE_SUBCLASS = 71;
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
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener);
}
private void processButtons(ByteBuffer buffer) {
byte b = buffer.get();
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
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);
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
leftTrigger = buffer.getShort() / 1023.0f;
rightTrigger = buffer.getShort() / 1023.0f;
leftStickX = buffer.getShort() / 32767.0f;
leftStickY = ~buffer.getShort() / 32767.0f;
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
}
private void ackModeReport(byte seqNum) {
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00};
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
}
@Override
protected boolean handleRead(ByteBuffer buffer) {
switch (buffer.get())
{
case 0x20:
buffer.position(buffer.position()+3);
processButtons(buffer);
return true;
case 0x07:
// The Xbox One S controller needs acks for mode reports otherwise
// it retransmits them forever.
if (buffer.get() == 0x30) {
ackModeReport(buffer.get());
buffer.position(buffer.position() + 1);
}
else {
buffer.position(buffer.position() + 2);
}
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
return true;
}
return false;
}
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() == XB1_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
return true;
}
}
return false;
}
@Override
protected boolean doInit() {
// Send the initialization packet
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
if (res != XB1_INIT_DATA.length) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
return true;
}
}
@@ -1,268 +0,0 @@
package com.limelight.binding.input.evdev;
import android.app.Activity;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class EvdevCaptureProvider extends InputCaptureProvider {
private final EvdevListener listener;
private final String libraryPath;
private boolean shutdown = false;
private InputStream evdevIn;
private OutputStream evdevOut;
private Process su;
private ServerSocket servSock;
private Socket evdevSock;
private Activity activity;
private boolean started = false;
private static final byte UNGRAB_REQUEST = 1;
private static final byte REGRAB_REQUEST = 2;
private final Thread handlerThread = new Thread() {
@Override
public void run() {
int deltaX = 0;
int deltaY = 0;
byte deltaScroll = 0;
// Bind a local listening socket for evdevreader to connect to
try {
servSock = new ServerSocket(0, 1);
} catch (IOException e) {
e.printStackTrace();
return;
}
// Launch a su shell
ProcessBuilder builder = new ProcessBuilder("su");
builder.redirectErrorStream(true);
try {
su = builder.start();
} catch (IOException e) {
reportDeviceNotRooted();
e.printStackTrace();
return;
}
// Start evdevreader
DataOutputStream suOut = new DataOutputStream(su.getOutputStream());
try {
suOut.writeChars(libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort()+"\n");
} catch (IOException e) {
reportDeviceNotRooted();
e.printStackTrace();
return;
}
// Wait for evdevreader's connection
LimeLog.info("Waiting for EvdevReader connection to port "+servSock.getLocalPort());
try {
evdevSock = servSock.accept();
evdevIn = evdevSock.getInputStream();
evdevOut = evdevSock.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
return;
}
LimeLog.info("EvdevReader connected from port "+evdevSock.getPort());
while (!isInterrupted() && !shutdown) {
EvdevEvent event;
try {
event = EvdevReader.read(evdevIn);
} catch (IOException e) {
event = null;
}
if (event == null) {
break;
}
switch (event.type) {
case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0;
}
if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll);
deltaScroll = 0;
}
break;
case EvdevEvent.EV_REL:
switch (event.code) {
case EvdevEvent.REL_X:
deltaX = event.value;
break;
case EvdevEvent.REL_Y:
deltaY = event.value;
break;
case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value;
break;
}
break;
case EvdevEvent.EV_KEY:
switch (event.code) {
case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0);
break;
case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0);
break;
case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0);
break;
case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons
break;
default:
// We got some unrecognized button. This means
// someone is trying to use the other device in this
// "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect
// if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode);
}
break;
}
break;
case EvdevEvent.EV_MSC:
break;
}
}
}
};
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
this.listener = listener;
this.activity = activity;
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
}
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
private void reportDeviceNotRooted() {
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();
}
}
}
}
@Override
public void disableCapture() {
if (started && !shutdown && evdevOut != null) {
try {
evdevOut.write(UNGRAB_REQUEST);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void destroy() {
// We need to stop the process in this context otherwise
// we could get stuck waiting on output from the process
// in order to terminate it.
if (!started) {
return;
}
shutdown = true;
handlerThread.interrupt();
if (servSock != null) {
try {
servSock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (evdevSock != null) {
try {
evdevSock.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (evdevIn != null) {
try {
evdevIn.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (evdevOut != null) {
try {
evdevOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (su != null) {
su.destroy();
}
try {
handlerThread.join();
} catch (InterruptedException ignored) {}
}
}
@@ -1,37 +1,41 @@
package com.limelight.binding.input.evdev;
public class EvdevEvent {
public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */
public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04;
/* Relative axes */
public static final short REL_X = 0x00;
public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08;
/* Buttons */
public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117;
public final short type;
public final short code;
public final int value;
public EvdevEvent(short type, short code, int value) {
this.type = type;
this.code = code;
this.value = value;
}
public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */
public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04;
/* Relative axes */
public static final short REL_X = 0x00;
public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08;
/* Buttons */
public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117;
public static final short BTN_GAMEPAD = 0x130;
/* Keys */
public static final short KEY_Q = 16;
public short type;
public short code;
public int value;
public EvdevEvent(short type, short code, int value) {
this.type = type;
this.code = code;
this.value = value;
}
}
@@ -0,0 +1,167 @@
package com.limelight.binding.input.evdev;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.limelight.LimeLog;
public class EvdevHandler {
private String absolutePath;
private EvdevListener listener;
private boolean shutdown = false;
private int fd = -1;
private Thread handlerThread = new Thread() {
@Override
public void run() {
// All the finally blocks here make this code look like a mess
// but it's important that we get this right to avoid causing
// system-wide input problems.
// Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath);
if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath);
return;
}
try {
// Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice
return;
}
// Grab it for ourselves
if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath);
return;
}
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
try {
int deltaX = 0;
int deltaY = 0;
byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) {
EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) {
return;
}
switch (event.type)
{
case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0;
}
if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll);
deltaScroll = 0;
}
break;
case EvdevEvent.EV_REL:
switch (event.code)
{
case EvdevEvent.REL_X:
deltaX = event.value;
break;
case EvdevEvent.REL_Y:
deltaY = event.value;
break;
case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value;
break;
}
break;
case EvdevEvent.EV_KEY:
switch (event.code)
{
case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0);
break;
case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0);
break;
case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0);
break;
case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons
break;
default:
// We got some unrecognized button. This means
// someone is trying to use the other device in this
// "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect
// if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode);
}
break;
}
break;
case EvdevEvent.EV_MSC:
break;
}
}
} finally {
// Release our grab
EvdevReader.ungrab(fd);
}
} finally {
// Close the file
EvdevReader.close(fd);
}
}
};
public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath;
this.listener = listener;
}
public void start() {
handlerThread.start();
}
public void stop() {
// Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from
// under the thread to wake it up
if (fd != -1) {
EvdevReader.close(fd);
}
shutdown = true;
handlerThread.interrupt();
try {
handlerThread.join();
} catch (InterruptedException ignored) {}
}
public void notifyDeleted() {
stop();
}
}
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev;
public interface EvdevListener {
public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode);
public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode);
}
@@ -1,58 +1,103 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Locale;
import com.limelight.LimeLog;
public class EvdevReader {
private static void readAll(InputStream in, ByteBuffer bb) throws IOException {
byte[] buf = bb.array();
int ret;
int offset = 0;
while (offset < buf.length) {
ret = in.read(buf, offset, buf.length-offset);
if (ret <= 0) {
throw new IOException("Read failed: "+ret);
}
offset += ret;
}
}
// Takes a byte buffer to use to read the output into.
// This buffer MUST be in native byte order and at least
// EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(InputStream input) throws IOException {
ByteBuffer bb;
int packetLength;
// Read the packet length
bb = ByteBuffer.allocate(4).order(ByteOrder.nativeOrder());
readAll(input, bb);
packetLength = bb.getInt();
if (packetLength < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+packetLength);
return null;
}
// Read the rest of the packet
bb = ByteBuffer.allocate(packetLength).order(ByteOrder.nativeOrder());
readAll(input, bb);
// Throw away the time stamp
if (packetLength == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
bb.getLong();
bb.getLong();
} else {
bb.getInt();
bb.getInt();
}
return new EvdevEvent(bb.getShort(), bb.getShort(), bb.getInt());
}
static {
System.loadLibrary("evdev_reader");
}
// Requires root to chmod /dev/input/eventX
public static boolean setPermissions(String[] files, int octalPermissions) {
ProcessBuilder builder = new ProcessBuilder("su");
try {
Process p = builder.start();
OutputStream stdin = p.getOutputStream();
for (String file : files) {
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
}
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
// Returns the fd to be passed to other function or -1 on error
public static native int open(String fileName);
// Prevent other apps (including Android itself) from using the device while "grabbed"
public static native boolean grab(int fd);
public static native boolean ungrab(int fd);
// Used for checking device capabilities
public static native boolean hasRelAxis(int fd, short axis);
public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key);
public static boolean isMouse(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasKey(fd, EvdevEvent.KEY_Q);
}
public static boolean isGamepad(int fd) {
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
}
// Returns the bytes read or -1 on error
private static native int read(int fd, byte[] buffer);
// Takes a byte buffer to use to read the output into.
// This buffer MUST be in native byte order and at least
// EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead);
return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead);
buffer.rewind();
// Throw away the time stamp
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
buffer.getLong();
buffer.getLong();
} else {
buffer.getInt();
buffer.getInt();
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
}
// Closes the fd from open()
public static native int close(int fd);
}
@@ -4,136 +4,136 @@ import android.view.KeyEvent;
public class EvdevTranslator {
private static final short[] EVDEV_KEY_CODES = {
0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT,
0,
0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
};
public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode];
}
return 0;
}
public static final short EVDEV_KEY_CODES[] = {
0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT,
0,
0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
};
public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode];
}
return 0;
}
}
@@ -0,0 +1,172 @@
package com.limelight.binding.input.evdev;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import com.limelight.LimeLog;
import android.os.FileObserver;
@SuppressWarnings("ALL")
public class EvdevWatcher {
private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event";
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false;
private boolean init = false;
private boolean ungrabbed = false;
private EvdevListener listener;
private Thread startThread;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override
public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
return;
}
synchronized (handlers) {
if (shutdown) {
return;
}
if ((event & FileObserver.CREATE) != 0) {
LimeLog.info("Starting evdev handler for "+fileName);
if (!init) {
// If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
// If we're ungrabbed now, don't start the handler
if (!ungrabbed) {
handler.start();
}
handlers.put(fileName, handler);
}
if ((event & FileObserver.DELETE) != 0) {
LimeLog.info("Halting evdev handler for "+fileName);
EvdevHandler handler = handlers.remove(fileName);
if (handler != null) {
handler.notifyDeleted();
}
}
}
}
};
public EvdevWatcher(EvdevListener listener) {
this.listener = listener;
}
private File[] rundownWithPermissionsChange(int newPermissions) {
// Rundown existing files
File devInputDir = new File(PATH);
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
// Set desired permissions
String[] filePaths = new String[files.length];
for (int i = 0; i < files.length; i++) {
filePaths[i] = files[i].getAbsolutePath();
}
EvdevReader.setPermissions(filePaths, newPermissions);
return files;
}
public void ungrabAll() {
synchronized (handlers) {
// Note that we're ungrabbed for now
ungrabbed = true;
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
public void regrabAll() {
synchronized (handlers) {
// We're regrabbing everything now
ungrabbed = false;
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
// We need to recreate each entry since we can't reuse a stopped one
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
entry.getValue().start();
}
}
}
public void start() {
startThread = new Thread() {
@Override
public void run() {
// List all files and allow us access
File[] files = rundownWithPermissionsChange(0666);
init = true;
for (File f : files) {
observer.onEvent(FileObserver.CREATE, f.getName());
}
// Done with initial onEvent calls
init = false;
// Start watching for new files
observer.startWatching();
synchronized (startThread) {
// Wait to be awoken again by shutdown()
try {
startThread.wait();
} catch (InterruptedException e) {}
}
// Giveup eventX permissions
rundownWithPermissionsChange(066);
}
};
startThread.start();
}
public void shutdown() {
// Let start thread cleanup on it's own sweet time
synchronized (startThread) {
startThread.notify();
}
// Stop the observer
observer.stopWatching();
synchronized (handlers) {
// Stop creating new handlers
shutdown = true;
// If we've already ungrabbed, there's nothing else to do
if (ungrabbed) {
return;
}
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
}
@@ -1,349 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
/**
* This is a analog stick on screen element. It is used to get 2-Axis user input.
*/
public class AnalogStick extends VirtualControllerElement {
/**
* outer radius size in percent of the ui element
*/
public static final int SIZE_RADIUS_COMPLETE = 90;
/**
* analog stick size in percent of the ui element
*/
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
/**
* dead zone size in percent of the ui element
*/
public static final int SIZE_RADIUS_DEADZONE = 90;
/**
* time frame for a double click
*/
public final static long timeoutDoubleClick = 350;
/**
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
*/
public final static long timeoutDeadzone = 150;
/**
* Listener interface to update registered observers.
*/
public interface AnalogStickListener {
/**
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
*
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
* @param y vertical position, value from -1.0 ... 0 .. 1.0
*/
void onMovement(float x, float y);
/**
* onClick event will be fired on click on the analog stick
*/
void onClick();
/**
* onDoubleClick event will be fired on a double click in a short time frame on the analog
* stick.
*/
void onDoubleClick();
/**
* onRevoke event will be fired on unpress of the analog stick.
*/
void onRevoke();
}
/**
* Movement states of the analog sick.
*/
private enum STICK_STATE {
NO_MOVEMENT,
MOVED_IN_DEAD_ZONE,
MOVED_ACTIVE
}
/**
* Click type states.
*/
private enum CLICK_STATE {
SINGLE,
DOUBLE
}
/**
* configuration if the analog stick should be displayed as circle or square
*/
private boolean circle_stick = true; // TODO: implement square sick for simulations
/**
* outer radius, this size will be automatically updated on resize
*/
private float radius_complete = 0;
/**
* analog stick radius, this size will be automatically updated on resize
*/
private float radius_analog_stick = 0;
/**
* dead zone radius, this size will be automatically updated on resize
*/
private float radius_dead_zone = 0;
/**
* horizontal position in relation to the center of the element
*/
private float relative_x = 0;
/**
* vertical position in relation to the center of the element
*/
private float relative_y = 0;
private double movement_radius = 0;
private double movement_angle = 0;
private float position_stick_x = 0;
private float position_stick_y = 0;
private final Paint paint = new Paint();
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
private List<AnalogStickListener> listeners = new ArrayList<>();
private long timeLastClick = 0;
private static double getMovementRadius(float x, float y) {
return Math.sqrt(x * x + y * y);
}
private static double getAngle(float way_x, float way_y) {
// prevent divisions by zero for corner cases
if (way_x == 0) {
return way_y < 0 ? Math.PI : 0;
} else if (way_y == 0) {
if (way_x > 0) {
return Math.PI * 3 / 2;
} else if (way_x < 0) {
return Math.PI * 1 / 2;
}
}
// return correct calculated angle for each quadrant
if (way_x > 0) {
if (way_y < 0) {
// first quadrant
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
} else {
// second quadrant
return Math.PI + Math.atan((double) (way_x / way_y));
}
} else {
if (way_y > 0) {
// third quadrant
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
} else {
// fourth quadrant
return 0 + Math.atan((double) (-way_x / -way_y));
}
}
}
public AnalogStick(VirtualController controller, Context context) {
super(controller, context);
// reset stick position
position_stick_x = getWidth() / 2;
position_stick_y = getHeight() / 2;
}
public void addAnalogStickListener(AnalogStickListener listener) {
listeners.add(listener);
}
private void notifyOnMovement(float x, float y) {
_DBG("movement x: " + x + " movement y: " + y);
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onMovement(x, y);
}
}
private void notifyOnClick() {
_DBG("click");
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onClick();
}
}
private void notifyOnDoubleClick() {
_DBG("double click");
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onDoubleClick();
}
}
private void notifyOnRevoke() {
_DBG("revoke");
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onRevoke();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 90);
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onElementDraw(Canvas canvas) {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(getDefaultStrokeWidth());
// draw outer circle
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
paint.setColor(getDefaultColor());
} else {
paint.setColor(pressedColor);
}
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
paint.setColor(getDefaultColor());
// draw dead zone
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
// draw stick depending on state
switch (stick_state) {
case NO_MOVEMENT: {
paint.setColor(getDefaultColor());
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
break;
}
case MOVED_IN_DEAD_ZONE:
case MOVED_ACTIVE: {
paint.setColor(pressedColor);
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
break;
}
}
}
private void updatePosition() {
// get 100% way
float complete = radius_complete - radius_analog_stick;
// calculate relative way
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
// update positions
position_stick_x = getWidth() / 2 - correlated_x;
position_stick_y = getHeight() / 2 - correlated_y;
// Stay active even if we're back in the deadzone because we know the user is actively
// giving analog stick input and we don't want to snap back into the deadzone.
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
// them to make precise movements.
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
movement_radius > radius_dead_zone) ?
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
// trigger move event if state active
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
}
}
@Override
public boolean onElementTouchEvent(MotionEvent event) {
// save last click state
CLICK_STATE lastClickState = click_state;
// get absolute way for each axis
relative_x = -(getWidth() / 2 - event.getX());
relative_y = -(getHeight() / 2 - event.getY());
// get radius and angel of movement from center
movement_radius = getMovementRadius(relative_x, relative_y);
movement_angle = getAngle(relative_x, relative_y);
// chop radius if out of outer circle and already pressed
if (movement_radius > (radius_complete - radius_analog_stick)) {
// not pressed already, so ignore event from outer circle
if (!isPressed()) {
return false;
}
movement_radius = radius_complete - radius_analog_stick;
}
// handle event depending on action
switch (event.getActionMasked()) {
// down event (touch event)
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
// set to dead zoned, will be corrected in update position if necessary
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
// check for double click
if (lastClickState == CLICK_STATE.SINGLE &&
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
click_state = CLICK_STATE.DOUBLE;
notifyOnDoubleClick();
} else {
click_state = CLICK_STATE.SINGLE;
notifyOnClick();
}
// reset last click timestamp
timeLastClick = System.currentTimeMillis();
// set item pressed and update
setPressed(true);
break;
}
// up event (revoke touch)
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
setPressed(false);
break;
}
}
if (isPressed()) {
// when is pressed calculate new positions (will trigger movement if necessary)
updatePosition();
} else {
stick_state = STICK_STATE.NO_MOVEMENT;
notifyOnRevoke();
// not longer pressed reset analog stick
notifyOnMovement(0, 0);
}
// refresh view
invalidate();
// accept the touch event
return true;
}
}
@@ -1,237 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
/**
* This is a digital button on screen element. It is used to get click and double click user input.
*/
public class DigitalButton extends VirtualControllerElement {
/**
* Listener interface to update registered observers.
*/
public interface DigitalButtonListener {
/**
* onClick event will be fired on button click.
*/
void onClick();
/**
* onLongClick event will be fired on button long click.
*/
void onLongClick();
/**
* onRelease event will be fired on button unpress.
*/
void onRelease();
}
/**
*
*/
private class TimerLongClickTimerTask extends TimerTask {
@Override
public void run() {
onLongClickCallback();
}
}
private List<DigitalButtonListener> listeners = new ArrayList<>();
private String text = "";
private int icon = -1;
private long timerLongClickTimeout = 3000;
private Timer timerLongClick = null;
private TimerLongClickTimerTask longClickTimerTask = null;
private final Paint paint = new Paint();
private int layer;
private DigitalButton movingButton = null;
boolean inRange(float x, float y) {
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
(this.getY() < y && this.getY() + this.getHeight() > y);
}
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
// check if the movement happened in the same layer
if (movingButton.layer != this.layer) {
return false;
}
// save current pressed state
boolean wasPressed = isPressed();
// check if the movement directly happened on the button
if ((this.movingButton == null || movingButton == this.movingButton)
&& this.inRange(x, y)) {
// set button pressed state depending on moving button pressed state
if (this.isPressed() != movingButton.isPressed()) {
this.setPressed(movingButton.isPressed());
}
}
// check if the movement is outside of the range and the movement button
// is the saved moving button
else if (movingButton == this.movingButton) {
this.setPressed(false);
}
// check if a change occurred
if (wasPressed != isPressed()) {
if (isPressed()) {
// is pressed set moving button and emit click event
this.movingButton = movingButton;
onClickCallback();
} else {
// no longer pressed reset moving button and emit release event
this.movingButton = null;
onReleaseCallback();
}
invalidate();
return true;
}
return false;
}
private void checkMovementForAllButtons(float x, float y) {
for (VirtualControllerElement element : virtualController.getElements()) {
if (element != this && element instanceof DigitalButton) {
((DigitalButton) element).checkMovement(x, y, this);
}
}
}
public DigitalButton(VirtualController controller, int layer, Context context) {
super(controller, context);
this.layer = layer;
}
public void addDigitalButtonListener(DigitalButtonListener listener) {
listeners.add(listener);
}
public void setText(String text) {
this.text = text;
invalidate();
}
public void setIcon(int id) {
this.icon = id;
invalidate();
}
@Override
protected void onElementDraw(Canvas canvas) {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setTextSize(getPercent(getWidth(), 30));
paint.setTextAlign(Paint.Align.CENTER);
paint.setStrokeWidth(getDefaultStrokeWidth());
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
if (icon != -1) {
Drawable d = getResources().getDrawable(icon);
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
d.draw(canvas);
} else {
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
}
}
private void onClickCallback() {
_DBG("clicked");
// notify listeners
for (DigitalButtonListener listener : listeners) {
listener.onClick();
}
timerLongClick = new Timer();
longClickTimerTask = new TimerLongClickTimerTask();
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
}
private void onLongClickCallback() {
_DBG("long click");
// notify listeners
for (DigitalButtonListener listener : listeners) {
listener.onLongClick();
}
}
private void onReleaseCallback() {
_DBG("released");
// notify listeners
for (DigitalButtonListener listener : listeners) {
listener.onRelease();
}
timerLongClick.cancel();
longClickTimerTask.cancel();
}
@Override
public boolean onElementTouchEvent(MotionEvent event) {
// get masked (not specific to a pointer) action
float x = getX() + event.getX();
float y = getY() + event.getY();
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
movingButton = null;
setPressed(true);
onClickCallback();
invalidate();
return true;
}
case MotionEvent.ACTION_MOVE: {
checkMovementForAllButtons(x, y);
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
setPressed(false);
onReleaseCallback();
checkMovementForAllButtons(x, y);
invalidate();
return true;
}
default: {
}
}
return true;
}
}
@@ -1,205 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
public class DigitalPad extends VirtualControllerElement {
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
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_DOWN = 8;
List<DigitalPadListener> listeners = new ArrayList<>();
private static final int DPAD_MARGIN = 5;
private final Paint paint = new Paint();
public DigitalPad(VirtualController controller, Context context) {
super(controller, context);
}
public void addDigitalPadListener(DigitalPadListener listener) {
listeners.add(listener);
}
@Override
protected void onElementDraw(Canvas canvas) {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setTextSize(getPercent(getCorrectWidth(), 20));
paint.setTextAlign(Paint.Align.CENTER);
paint.setStrokeWidth(getDefaultStrokeWidth());
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
// draw no direction rect
paint.setStyle(Paint.Style.STROKE);
paint.setColor(getDefaultColor());
canvas.drawRect(
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
paint
);
}
// draw left rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
paint
);
// draw up rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
paint
);
// draw right rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
paint
);
// draw down rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
paint
);
// draw left up line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
paint
);
// draw up right line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
paint
);
// draw right down line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
paint
);
// draw down left line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
paint
);
}
private void newDirectionCallback(int direction) {
_DBG("direction: " + direction);
// notify listeners
for (DigitalPadListener listener : listeners) {
listener.onDirectionChange(direction);
}
}
@Override
public boolean onElementTouchEvent(MotionEvent event) {
// get masked (not specific to a pointer) action
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_MOVE: {
direction = 0;
if (event.getX() < getPercent(getWidth(), 33)) {
direction |= DIGITAL_PAD_DIRECTION_LEFT;
}
if (event.getX() > getPercent(getWidth(), 66)) {
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
}
if (event.getY() > getPercent(getHeight(), 66)) {
direction |= DIGITAL_PAD_DIRECTION_DOWN;
}
if (event.getY() < getPercent(getHeight(), 33)) {
direction |= DIGITAL_PAD_DIRECTION_UP;
}
newDirectionCallback(direction);
invalidate();
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
direction = 0;
newDirectionCallback(direction);
invalidate();
return true;
}
default: {
}
}
return true;
}
public interface DigitalPadListener {
void onDirectionChange(int direction);
}
}
@@ -1,49 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import com.limelight.nvstream.input.ControllerPacket;
public class LeftAnalogStick extends AnalogStick {
public LeftAnalogStick(final VirtualController controller, final Context context) {
super(controller, context);
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
@Override
public void onMovement(float x, float y) {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.leftStickX = (short) (x * 0x7FFE);
inputContext.leftStickY = (short) (y * 0x7FFE);
controller.sendControllerInputContext();
}
@Override
public void onClick() {
}
@Override
public void onDoubleClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
controller.sendControllerInputContext();
}
@Override
public void onRevoke() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
controller.sendControllerInputContext();
}
});
}
}
@@ -1,36 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
public class LeftTrigger extends DigitalButton {
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
super(controller, layer, context);
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.leftTrigger = (byte) 0xFF;
controller.sendControllerInputContext();
}
@Override
public void onLongClick() {
}
@Override
public void onRelease() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.leftTrigger = (byte) 0x00;
controller.sendControllerInputContext();
}
});
}
}
@@ -1,49 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import com.limelight.nvstream.input.ControllerPacket;
public class RightAnalogStick extends AnalogStick {
public RightAnalogStick(final VirtualController controller, final Context context) {
super(controller, context);
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
@Override
public void onMovement(float x, float y) {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.rightStickX = (short) (x * 0x7FFE);
inputContext.rightStickY = (short) (y * 0x7FFE);
controller.sendControllerInputContext();
}
@Override
public void onClick() {
}
@Override
public void onDoubleClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
controller.sendControllerInputContext();
}
@Override
public void onRevoke() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
controller.sendControllerInputContext();
}
});
}
}
@@ -1,36 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
public class RightTrigger extends DigitalButton {
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
super(controller, layer, context);
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.rightTrigger = (byte) 0xFF;
controller.sendControllerInputContext();
}
@Override
public void onLongClick() {
}
@Override
public void onRelease() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.rightTrigger = (byte) 0x00;
controller.sendControllerInputContext();
}
});
}
}
@@ -1,162 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.limelight.R;
import com.limelight.nvstream.NvConnection;
import java.util.ArrayList;
import java.util.List;
public class VirtualController {
public class ControllerInputContext {
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
public short rightStickX = 0x0000;
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
}
public enum ControllerMode {
Active,
Configuration
}
private static final boolean _PRINT_DEBUG_INFORMATION = false;
private NvConnection connection = null;
private Context context = null;
private FrameLayout frame_layout = null;
private RelativeLayout relative_layout = null;
ControllerMode currentMode = ControllerMode.Active;
ControllerInputContext inputContext = new ControllerInputContext();
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
private Button buttonConfigure = null;
private List<VirtualControllerElement> elements = new ArrayList<>();
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
this.connection = conn;
this.frame_layout = layout;
this.context = context;
relative_layout = new RelativeLayout(context);
frame_layout.addView(relative_layout);
buttonConfigure = new Button(context);
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
buttonConfigure.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message;
if (currentMode == ControllerMode.Configuration) {
currentMode = ControllerMode.Active;
message = "Exiting configuration mode";
} else {
currentMode = ControllerMode.Configuration;
message = "Entering configuration mode";
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
relative_layout.invalidate();
for (VirtualControllerElement element : elements) {
element.invalidate();
}
}
});
}
public void removeElements() {
for (VirtualControllerElement element : elements) {
relative_layout.removeView(element);
}
elements.clear();
}
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
elements.add(element);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
layoutParams.setMargins(x, y, 0, 0);
relative_layout.addView(element, layoutParams);
}
public List<VirtualControllerElement> getElements() {
return elements;
}
private static final void _DBG(String text) {
if (_PRINT_DEBUG_INFORMATION) {
System.out.println("VirtualController: " + text);
}
}
public void refreshLayout() {
relative_layout.removeAllViews();
removeElements();
DisplayMetrics screen = context.getResources().getDisplayMetrics();
int buttonSize = (int)(screen.heightPixels*0.05f);
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
}
public ControllerMode getControllerMode() {
return currentMode;
}
public ControllerInputContext getControllerInputContext() {
return inputContext;
}
public void sendControllerInputContext() {
sendControllerInputPacket();
}
private void sendControllerInputPacket() {
try {
_DBG("INPUT_MAP + " + inputContext.inputMap);
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
if (connection != null) {
connection.sendControllerInput(
inputContext.inputMap,
inputContext.leftTrigger,
inputContext.rightTrigger,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY
);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -1,285 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.util.DisplayMetrics;
import com.limelight.nvstream.input.ControllerPacket;
public class VirtualControllerConfigurationLoader {
private static final String PROFILE_PATH = "profiles";
private static int getPercent(
int percent,
int total) {
return (int) (((float) total / (float) 100) * (float) percent);
}
private static DigitalPad createDigitalPad(
final VirtualController controller,
final Context context) {
DigitalPad digitalPad = new DigitalPad(controller, context);
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
@Override
public void onDirectionChange(int direction) {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
controller.sendControllerInputContext();
return;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
inputContext.inputMap |= ControllerPacket.UP_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
}
controller.sendControllerInputContext();
}
});
return digitalPad;
}
private static DigitalButton createDigitalButton(
final int keyShort,
final int keyLong,
final int layer,
final String text,
final int icon,
final VirtualController controller,
final Context context) {
DigitalButton button = new DigitalButton(controller, layer, context);
button.setText(text);
button.setIcon(icon);
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= keyShort;
controller.sendControllerInputContext();
}
@Override
public void onLongClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= keyLong;
controller.sendControllerInputContext();
}
@Override
public void onRelease() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap &= ~keyShort;
inputContext.inputMap &= ~keyLong;
controller.sendControllerInputContext();
}
});
return button;
}
private static DigitalButton createLeftTrigger(
final int layer,
final String text,
final int icon,
final VirtualController controller,
final Context context) {
LeftTrigger button = new LeftTrigger(controller, layer, context);
button.setText(text);
button.setIcon(icon);
return button;
}
private static DigitalButton createRightTrigger(
final int layer,
final String text,
final int icon,
final VirtualController controller,
final Context context) {
RightTrigger button = new RightTrigger(controller, layer, context);
button.setText(text);
button.setIcon(icon);
return button;
}
private static AnalogStick createLeftStick(
final VirtualController controller,
final Context context) {
return new LeftAnalogStick(controller, context);
}
private static AnalogStick createRightStick(
final VirtualController controller,
final Context context) {
return new RightAnalogStick(controller, context);
}
private static final int BUTTON_BASE_X = 65;
private static final int BUTTON_BASE_Y = 5;
private static final int BUTTON_WIDTH = getPercent(30, 33);
private static final int BUTTON_HEIGHT = getPercent(40, 33);
public static void createDefaultLayout(final VirtualController controller, final Context context) {
DisplayMetrics screen = context.getResources().getDisplayMetrics();
// NOTE: Some of these getPercent() expressions seem like they can be combined
// into a single call. Due to floating point rounding, this isn't actually possible.
controller.addElement(createDigitalPad(controller, context),
getPercent(5, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(30, screen.widthPixels),
getPercent(40, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftTrigger(
0, "LT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createRightTrigger(
0, "RT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftStick(controller, context),
getPercent(5, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createRightStick(controller, context),
getPercent(55, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
getPercent(40, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
}
/*
NOT IMPLEMENTED YET,
this should later be used to store and load a profile for the virtual controller
public static void saveProfile(final String name,
final VirtualController controller,
final Context context) {
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
name, Activity.MODE_PRIVATE);
JSONArray elementConfigurations = new JSONArray();
for (VirtualControllerElement element : controller.getElements()) {
JSONObject elementConfiguration = new JSONObject();
try {
elementConfiguration.put("TYPE", element.getClass().getName());
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
elementConfigurations.put(elementConfiguration);
} catch (Exception e) {
e.printStackTrace();
}
}
SharedPreferences.Editor editor= preferences.edit();
editor.putString("ELEMENTS", elementConfigurations.toString());
}
public static void loadFromPreferences(final VirtualController controller) {
}
*/
}
@@ -1,290 +0,0 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
public abstract class VirtualControllerElement extends View {
protected static boolean _PRINT_DEBUG_INFORMATION = false;
protected VirtualController virtualController;
private final Paint paint = new Paint();
private int normalColor = 0xF0888888;
protected int pressedColor = 0xF00000FF;
private int configNormalColor = 0xF0FF0000;
private int configSelectedColor = 0xF000FF00;
protected int startSize_x;
protected int startSize_y;
float position_pressed_x = 0;
float position_pressed_y = 0;
private enum Mode {
Normal,
Resize,
Move
}
private Mode currentMode = Mode.Normal;
protected VirtualControllerElement(VirtualController controller, Context context) {
super(context);
this.virtualController = controller;
}
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
int newPos_x = (int) getX() + x - pressed_x;
int newPos_y = (int) getY() + y - pressed_y;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
layoutParams.rightMargin = 0;
layoutParams.bottomMargin = 0;
requestLayout();
}
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
int newHeight = height + (startSize_y - pressed_y);
int newWidth = width + (startSize_x - pressed_x);
layoutParams.height = newHeight > 20 ? newHeight : 20;
layoutParams.width = newWidth > 20 ? newWidth : 20;
requestLayout();
}
@Override
protected void onDraw(Canvas canvas) {
onElementDraw(canvas);
if (currentMode != Mode.Normal) {
paint.setColor(configSelectedColor);
paint.setStrokeWidth(getDefaultStrokeWidth());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
paint);
}
super.onDraw(canvas);
}
/*
protected void actionShowNormalColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override
public void onCancel(AmbilWarnaDialog dialog)
{}
@Override
public void onOk(AmbilWarnaDialog dialog, int color) {
normalColor = color;
invalidate();
}
});
colorDialog.show();
}
protected void actionShowPressedColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override
public void onCancel(AmbilWarnaDialog dialog) {
}
@Override
public void onOk(AmbilWarnaDialog dialog, int color) {
pressedColor = color;
invalidate();
}
});
colorDialog.show();
}
*/
protected void actionEnableMove() {
currentMode = Mode.Move;
}
protected void actionEnableResize() {
currentMode = Mode.Resize;
}
protected void actionCancel() {
currentMode = Mode.Normal;
invalidate();
}
protected int getDefaultColor() {
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
configNormalColor : normalColor;
}
protected int getDefaultStrokeWidth() {
DisplayMetrics screen = getResources().getDisplayMetrics();
return (int)(screen.heightPixels*0.004f);
}
protected void showConfigurationDialog() {
try {
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
alertBuilder.setTitle("Configuration");
CharSequence functions[] = new CharSequence[]{
"Move",
"Resize",
/*election
"Set n
Disable color sormal color",
"Set pressed color",
*/
"Cancel"
};
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0: { // move
actionEnableMove();
break;
}
case 1: { // resize
actionEnableResize();
break;
}
/*
case 2: { // set default color
actionShowNormalColorChooser();
break;
}
case 3: { // set pressed color
actionShowPressedColorChooser();
break;
}
*/
default: { // cancel
actionCancel();
break;
}
}
}
});
AlertDialog alert = alertBuilder.create();
// show menu
alert.show();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
return onElementTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
position_pressed_x = event.getX();
position_pressed_y = event.getY();
startSize_x = getWidth();
startSize_y = getHeight();
actionEnableMove();
return true;
}
case MotionEvent.ACTION_MOVE: {
switch (currentMode) {
case Move: {
moveElement(
(int) position_pressed_x,
(int) position_pressed_y,
(int) event.getX(),
(int) event.getY());
break;
}
case Resize: {
resizeElement(
(int) position_pressed_x,
(int) position_pressed_y,
(int) event.getX(),
(int) event.getY());
break;
}
case Normal: {
break;
}
}
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
actionCancel();
return true;
}
default: {
}
}
return true;
}
abstract protected void onElementDraw(Canvas canvas);
abstract public boolean onElementTouchEvent(MotionEvent event);
protected static final void _DBG(String text) {
if (_PRINT_DEBUG_INFORMATION) {
System.out.println(text);
}
}
public void setColors(int normalColor, int pressedColor) {
this.normalColor = normalColor;
this.pressedColor = pressedColor;
invalidate();
}
protected final float getPercent(float value, float percent) {
return value / 100 * percent;
}
protected final int getCorrectWidth() {
return getWidth() > getHeight() ? getHeight() : getWidth();
}
/**
public JSONObject getConfiguration () {
JSONObject configuration = new JSONObject();
return configuration;
}
public void loadConfiguration (JSONObject configuration) {
}
*/
}
@@ -0,0 +1,263 @@
package com.limelight.binding.video;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import android.graphics.PixelFormat;
import android.os.Build;
import android.view.SurfaceHolder;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread;
private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 8;
private static final int LOW_PERF = 1;
private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3;
private int totalFrames;
private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
// Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection
// of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) {
// Emulator
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15
return MED_PERF;
}
else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7
return LOW_PERF;
}
else {
// Didn't have anything we're looking for
return MED_PERF;
}
} catch (IOException e) {
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {}
}
}
// Couldn't read cpuinfo, so assume medium
return MED_PERF;
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
int avcFlags = 0;
switch (perfLevel) {
case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1;
break;
case LOW_PERF:
// Disable the loop filter for performance reasons
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1;
break;
default:
case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency
threadCount = 2;
break;
}
// If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding");
}
SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err);
}
AvcDecoder.setRenderTarget(sh.getSurface());
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override
public boolean start(final VideoDepacketizer depacketizer) {
rendererThread = new Thread() {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted())
{
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) {
LockSupport.parkNanos(1);
continue;
}
nextFrameTime = computePresentationTimeMs(targetFps);
AvcDecoder.redraw();
}
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
private long computePresentationTimeMs(int frameRate) {
return System.currentTimeMillis() + (1000 / frameRate);
}
@Override
public void stop() {
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException e) { }
}
@Override
public void release() {
AvcDecoder.destroy();
}
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
byte[] data;
// Use the reserved decoder buffer if this decode unit will fit
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
data = decoderBuffer.array();
}
else {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += delta;
totalFrames++;
}
}
return success;
}
@Override
public int getCapabilities() {
return 0;
}
@Override
public int getAverageDecoderLatency() {
return 0;
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
}
@@ -0,0 +1,77 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
private VideoDecoderRenderer decoderRenderer;
@Override
public void release() {
if (decoderRenderer != null) {
decoderRenderer.release();
}
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
}
public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
}
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
}
public boolean isHardwareAccelerated() {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
}
@Override
public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer);
}
@Override
public void stop() {
decoderRenderer.stop();
}
@Override
public int getCapabilities() {
return decoderRenderer.getCapabilities();
}
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency();
}
else {
return 0;
}
}
@Override
public int getAverageEndToEndLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency();
}
else {
return 0;
}
}
}
@@ -1,8 +0,0 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract boolean isHevcSupported();
public abstract boolean isAvcSupported();
}
File diff suppressed because it is too large Load Diff
@@ -10,9 +10,6 @@ import java.util.Locale;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCodecInfo.CodecCapabilities;
@@ -23,109 +20,35 @@ import com.limelight.LimeLog;
public class MediaCodecHelper {
private static final List<String> preferredDecoders;
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
private static final List<String> whitelistedHevcDecoders;
static {
directSubmitPrefixes = new LinkedList<>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
}
public static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
static {
preferredDecoders = new LinkedList<>();
preferredDecoders = new LinkedList<String>();
}
static {
blacklistedDecoderPrefixes = new LinkedList<>();
blacklistedDecoderPrefixes = new LinkedList<String>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
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 {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
constrainedHighProfilePrefixes = new LinkedList<>();
constrainedHighProfilePrefixes.add("omx.intel");
}
static {
whitelistedHevcDecoders = new LinkedList<>();
// Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos");
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
//whitelistedHevcDecoders.add("omx.nvidia");
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
// to actually be tested. Ugh...
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
whitelistedHevcDecoders.add("omx.mtk");
}
// These theoretically have good HEVC decoding capabilities (potentially better than
// their AVC decoders), but haven't been tested enough
//whitelistedHevcDecoders.add("omx.amlogic");
//whitelistedHevcDecoders.add("omx.rk");
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
// during initialization to avoid SoCs with broken HEVC decoders.
}
public static void initializeWithContext(Context context) {
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
// tell the good from the bad decoders are the generation of Adreno GPU included:
// 3xx - bad
// 4xx - good
//
// Unfortunately, it's not that easy to get that information here, so I'll use an
// approximation by checking the GLES level (<= 3.0 is bad).
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
if (configInfo.reqGlEsVersion > 0x30000) {
LimeLog.info("Added omx.qcom to supported decoders based on GLES 3.1+ support");
whitelistedHevcDecoders.add("omx.qcom");
}
}
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
@@ -140,17 +63,9 @@ public class MediaCodecHelper {
return false;
}
public static long getMonotonicMillis() {
return System.nanoTime() / 1000000L;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
/*
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
so we'll keep it off for now, since we don't know whether other devices also do the same
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
@@ -169,56 +84,19 @@ public class MediaCodecHelper {
} catch (Exception e) {
// Tolerate buggy codecs
}
}*/
}
return false;
}
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
}
public static boolean decoderCanDirectSubmit(String decoderName) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
// TODO: Shield Tablet K1/LTE?
//
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
// whether the performance is good enough to use for streaming, but they're
// using the same omx.nvidia.h265.decode name as the Shield TV which has a
// fully accelerated HEVC pipeline. AFAIK, the only K1 device with this
// partially accelerated HEVC decoder is the Shield Tablet, so I'll
// check for it here.
//
// TODO: Temporarily disabled with NVIDIA HEVC support
/*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) {
return false;
}*/
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
// so I'm restricting HEVC usage to Lollipop and higher.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}
return isDecoderInList(whitelistedHevcDecoders, decoderName);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
@@ -255,7 +133,7 @@ public class MediaCodecHelper {
return str;
}
private static MediaCodecInfo findPreferredDecoder() {
public static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
@@ -278,7 +156,7 @@ public class MediaCodecHelper {
return null;
}
public static MediaCodecInfo findFirstDecoder(String mimeType) {
public static MediaCodecInfo findFirstDecoder() {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@@ -291,9 +169,9 @@ public class MediaCodecHelper {
continue;
}
// Find a decoder that supports the specified video format
// Find a decoder that supports H.264
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
@@ -303,7 +181,7 @@ public class MediaCodecHelper {
return null;
}
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
public static MediaCodecInfo findProbableSafeDecoder() {
// First look for a preferred decoder by name
MediaCodecInfo info = findPreferredDecoder();
if (info != null) {
@@ -313,12 +191,12 @@ public class MediaCodecHelper {
// Now look for decoders we know are safe
try {
// If this function completes, it will determine if the decoder is safe
return findKnownSafeDecoder(mimeType, requiredProfile);
return findKnownSafeDecoder();
} catch (Exception e) {
// Some buggy devices seem to throw exceptions
// from getCapabilitiesForType() so we'll just assume
// they're okay and go with the first one we find
return findFirstDecoder(mimeType);
return findFirstDecoder();
}
}
@@ -326,7 +204,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@@ -339,26 +217,21 @@ public class MediaCodecHelper {
continue;
}
// Find a decoder that supports the requested video format
// Find a decoder that supports H.264 high profile
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
if (requiredProfile != -1) {
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == requiredProfile) {
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
return codecInfo;
}
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
}
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
}
else {
return codecInfo;
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}
@@ -376,7 +249,7 @@ public class MediaCodecHelper {
break;
cpuInfo.append((char)ch);
}
return cpuInfo.toString();
} finally {
br.close();
@@ -17,153 +17,148 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac";
private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac";
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
try {
// Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) {
// Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
}
initializeDb();
}
public void close() {
computerDb.close();
}
private void initializeDb() {
// Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
}
public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
// If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
ComputerDetails details = new ComputerDetails();
if (!c.moveToFirst()) {
// No matching computer
c.close();
return null;
}
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
try {
// Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) {
// Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
}
initializeDb();
}
public void close() {
computerDb.close();
}
private void initializeDb() {
// Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
}
public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
ComputerDetails details = new ComputerDetails();
if (!c.moveToFirst()) {
// No matching computer
c.close();
return null;
}
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
c.close();
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
c.close();
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
}
@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details);
public void notifyComputerUpdated(ComputerDetails details);
}
File diff suppressed because it is too large Load Diff
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
import android.content.Context;
public class IdentityManager {
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId;
public IdentityManager(Context c) {
uniqueId = loadUniqueId(c);
if (uniqueId == null) {
uniqueId = generateNewUniqueId(c);
}
LimeLog.info("UID is now: "+uniqueId);
}
public String getUniqueId() {
return uniqueId;
}
private static String loadUniqueId(Context c) {
// 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null;
LimeLog.info("Reading UID from disk");
try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{
LimeLog.severe("UID file data is truncated");
return null;
}
return new String(uid);
} catch (FileNotFoundException e) {
LimeLog.info("No UID file found");
return null;
} catch (IOException e) {
LimeLog.severe("Error while reading UID file");
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {}
}
}
}
private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string
LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr);
LimeLog.info("UID written to disk");
} catch (IOException e) {
LimeLog.severe("Error while writing UID file");
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {}
}
}
// We can return a UID even if I/O fails
return uidStr;
}
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId;
public IdentityManager(Context c) {
uniqueId = loadUniqueId(c);
if (uniqueId == null) {
uniqueId = generateNewUniqueId(c);
}
LimeLog.info("UID is now: "+uniqueId);
}
public String getUniqueId() {
return uniqueId;
}
private static String loadUniqueId(Context c) {
// 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null;
LimeLog.info("Reading UID from disk");
try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{
LimeLog.severe("UID file data is truncated");
return null;
}
return new String(uid);
} catch (FileNotFoundException e) {
LimeLog.info("No UID file found");
return null;
} catch (IOException e) {
LimeLog.severe("Error while reading UID file");
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {}
}
}
}
private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string
LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr);
LimeLog.info("UID written to disk");
} catch (IOException e) {
LimeLog.severe("Error while writing UID file");
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {}
}
}
// We can return a UID even if I/O fails
return uidStr;
}
}
@@ -15,76 +15,76 @@ import android.os.Binder;
import android.os.IBinder;
public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private final DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
}
@@ -1,116 +0,0 @@
package com.limelight.grid;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.grid.assets.CachedAppAssetLoader;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader;
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()));
}
public void cancelQueuedOperations() {
loader.cancelForegroundLoads();
loader.cancelBackgroundLoads();
loader.freeCacheMemory();
}
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
}
});
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app);
sortList();
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
}
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView);
return true;
}
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.app.getIsRunning()) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.ic_play);
return true;
}
// No overlay
return false;
}
@Override
public boolean shouldShowProgressBar(AppView.AppObject obj) {
return true;
}
}
@@ -1,91 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<>();
protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId) {
this.context = context;
this.layoutId = layoutId;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void clear() {
itemList.clear();
}
@Override
public int getCount() {
return itemList.size();
}
@Override
public Object getItem(int i) {
return itemList.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
public abstract boolean populateImageView(ImageView imgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
public abstract boolean shouldShowProgressBar(T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
convertView = inflater.inflate(layoutId, viewGroup, false);
}
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
if (prgView != null) {
if (shouldShowProgressBar(itemList.get(i))) {
prgView.setVisibility(View.VISIBLE);
}
else {
prgView.setVisibility(View.INVISIBLE);
}
}
if (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageBitmap(null);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
return convertView;
}
}
@@ -1,79 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
}
public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
}
});
}
public boolean removeComputer(PcView.ComputerObject computer) {
return itemList.remove(computer);
}
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.ONLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
imgView.setImageResource(R.drawable.ic_computer);
return true;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
else {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
return true;
}
return false;
}
@Override
public boolean shouldShowProgressBar(PcView.ComputerObject obj) {
// Still refreshing this PC so display the progress bar
return obj.details.reachability == ComputerDetails.Reachability.UNKNOWN;
}
}
@@ -1,338 +0,0 @@
package com.limelight.grid.assets;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.widget.ImageView;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer;
private final double scalingDivider;
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
public void cancelBackgroundLoads() {
Runnable r;
while ((r = cacheExecutor.getQueue().poll()) != null) {
cacheExecutor.remove(r);
}
}
public void cancelForegroundLoads() {
Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r);
}
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
return null;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
}
else {
// Otherwise it's a background load and we return nothing
return null;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
return null;
}
}
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<>(imageView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
}
// Cache the bitmap
if (bmp != null) {
memoryLoader.populateCache(tuple, bmp);
}
return bmp;
}
@Override
protected void onProgressUpdate(Void... nothing) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the view
imageView.setAlpha(1.0f);
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<LoaderTask> loaderTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<>(loaderTask);
}
public LoaderTask getLoaderTask() {
return loaderTaskReference.get();
}
}
private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getLoaderTask();
}
return null;
}
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
final LoaderTask loaderTask = getLoaderTask(imageView);
// Check if any task was pending for this image view
if (loaderTask != null && !loaderTask.isCancelled()) {
final LoaderTuple taskTuple = loaderTask.tuple;
// Cancel the task if it's not already loading the same data
if (taskTuple == null || !taskTuple.equals(tuple)) {
loaderTask.cancel(true);
} else {
// It's already loading what we want
return false;
}
}
// Allow the load to proceed
return true;
}
public void queueCacheLoad(NvApp app) {
final LoaderTuple tuple = new LoaderTuple(computer, app);
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
// It's in memory which means it must also be on disk
return;
}
// Queue a fetch in the cache executor
cacheExecutor.execute(new Runnable() {
@Override
public void run() {
// Check if the image is cached on disk
if (diskLoader.checkCacheExists(tuple)) {
return;
}
// Try to load the asset from the network and cache result on disk
doNetworkAssetLoad(tuple, null);
}
});
}
public boolean populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return true;
}
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return true;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
return false;
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LoaderTuple)) {
return false;
}
LoaderTuple other = (LoaderTuple) o;
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
}
@@ -1,82 +0,0 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
// 5 MB
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
// Make sure the cached asset doesn't exceed the maximum size
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
return null;
}
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (IOException ignored) {
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
boolean success = false;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
success = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
}
}
}
@@ -1,37 +0,0 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
}
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
memoryCache.evictAll();
}
}
@@ -1,49 +0,0 @@
package com.limelight.grid.assets;
import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
private final String uniqueId;
public NetworkAssetLoader(Context context, String uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
}
else {
LimeLog.info("Network asset load failed: " + tuple);
}
return in;
}
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
}
@@ -0,0 +1,44 @@
package com.limelight.nvstream.av.video.cpu;
public class AvcDecoder {
static {
// FFMPEG dependencies
System.loadLibrary("avutil-52");
System.loadLibrary("swresample-0");
System.loadLibrary("swscale-2");
System.loadLibrary("avcodec-55");
System.loadLibrary("avformat-55");
System.loadLibrary("nv_avc_dec");
}
/** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen);
}
@@ -2,180 +2,149 @@ package com.limelight.preferences;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
private Thread addThread;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
private Button addPcButton;
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
private void doAddPc(String host) {
String msg;
boolean finish = false;
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
}
else {
msg = "Successfully added computer";
finish = true;
}
} catch (UnknownHostException e) {
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
}
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
private void doAddPc(String host) {
String msg;
boolean finish = false;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false);
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){
msg = getResources().getString(R.string.addpc_fail);
}
else {
msg = getResources().getString(R.string.addpc_success);
finish = true;
}
} catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host);
}
dialog.dismiss();
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this);
this.hostText = (TextView) findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostText.getText().toString().trim());
}
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
return false;
}
return false;
}
});
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_computer_manually);
this.addPcButton = (Button) findViewById(R.id.addPc);
this.hostText = (TextView) findViewById(R.id.hostTextView);
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
addPcButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
computersToAdd.add(hostText.getText().toString());
}
});
}
}
@@ -2,62 +2,38 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String DECODER_PREF_STRING = "list_decoders";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final int BITRATE_DEFAULT_4K_30 = 40;
private static final int BITRATE_DEFAULT_4K_60 = 80;
private static final int BITRATE_DEFAULT_1080_60 = 30;
private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
public static final int FORCE_H265_OFF = 1;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
public int width, height, fps;
public int bitrate;
public int videoFormat;
public int deadzonePercentage;
public int decoder;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
@@ -72,11 +48,27 @@ public class PreferenceConfiguration {
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else if (resFpsString.equals("4K30")) {
return BITRATE_DEFAULT_4K_30;
else {
// Should never get here
return DEFAULT_BITRATE;
}
else if (resFpsString.equals("4K60")) {
return BITRATE_DEFAULT_4K_60;
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (str.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (str.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (str.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
@@ -84,47 +76,22 @@ public class PreferenceConfiguration {
}
}
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null) {
// TVs shouldn't use small mode by default
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
return false;
}
// API 21 uses LEANBACK instead of TELEVISION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return false;
}
}
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
}
private static int getVideoFormatValue(Context context) {
private static int getDecoderValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
if (str.equals("auto")) {
return AUTOSELECT_H265;
return AUTOSELECT_DECODER;
}
else if (str.equals("forceh265")) {
return FORCE_H265_ON;
else if (str.equals("software")) {
return FORCE_SOFTWARE_DECODER;
}
else if (str.equals("neverh265")) {
return FORCE_H265_OFF;
else if (str.equals("hardware")) {
return FORCE_HARDWARE_DECODER;
}
else {
// Should never get here
return AUTOSELECT_H265;
return AUTOSELECT_DECODER;
}
}
@@ -154,16 +121,6 @@ public class PreferenceConfiguration {
config.height = 1080;
config.fps = 60;
}
else if (str.equals("4K30")) {
config.width = 3840;
config.height = 2160;
config.fps = 30;
}
else if (str.equals("4K60")) {
config.width = 3840;
config.height = 2160;
config.fps = 60;
}
else {
// Should never get here
config.width = 1280;
@@ -171,23 +128,13 @@ public class PreferenceConfiguration {
config.fps = 60;
}
config.videoFormat = getVideoFormatValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
config.decoder = getDecoderValue(context);
// Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
return config;
}
@@ -19,15 +19,11 @@ public class SeekBarPreference extends DialogPreference
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private SeekBar seekBar;
private TextView valueText;
private final Context context;
private TextView splashText, valueText;
private Context context;
private final String dialogMessage;
private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
private String dialogMessage, suffix;
private int defaultValue, maxValue, currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -51,10 +47,9 @@ public class SeekBarPreference extends DialogPreference
suffix = context.getString(suffixId);
}
// Get default, min, and max seekbar values
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
// Get default and max seekbar values
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
}
@Override
@@ -65,7 +60,7 @@ public class SeekBarPreference extends DialogPreference
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6);
TextView splashText = new TextView(context);
splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) {
splashText.setText(dialogMessage);
@@ -76,7 +71,7 @@ public class SeekBarPreference extends DialogPreference
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.FILL_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params);
@@ -84,13 +79,8 @@ public class SeekBarPreference extends DialogPreference
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
if (value < minValue) {
seekBar.setProgress(minValue);
return;
}
String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
valueText.setText(suffix == null ? t : t.concat(" " + suffix));
}
@Override
@@ -100,7 +90,7 @@ public class SeekBarPreference extends DialogPreference
public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue);
@@ -131,6 +121,13 @@ public class SeekBarPreference extends DialogPreference
}
}
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) {
this.currentValue = progress;
if (seekBar != null) {
@@ -152,10 +149,10 @@ public class SeekBarPreference extends DialogPreference
if (shouldPersist()) {
currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress());
callChangeListener(seekBar.getProgress());
callChangeListener(Integer.valueOf(seekBar.getProgress()));
}
getDialog().dismiss();
((AlertDialog) getDialog()).dismiss();
}
});
}
@@ -1,21 +0,0 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
public class SmallIconCheckboxPreference extends CheckBoxPreference {
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return PreferenceConfiguration.getDefaultSmallMode(getContext());
}
}
@@ -1,59 +1,23 @@
package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.app.Activity;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
previousPrefs = PreferenceConfiguration.readPreferences(this);
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(previousPrefs.language);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
UiHelper.notifyNewRootView(this);
}
@Override
public void onBackPressed() {
finish();
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
}
public static class SettingsFragment extends PreferenceFragment {
@@ -62,15 +26,6 @@ public class StreamSettings extends Activity {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
PreferenceScreen screen = getPreferenceScreen();
// hide on-screen controls category on non touch screen devices
if (!getActivity().getPackageManager().
hasSystemFeature("android.hardware.touchscreen")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
}
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
@@ -1,35 +0,0 @@
package com.limelight.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import com.limelight.R;
public class AdapterFragment extends Fragment {
private AdapterFragmentCallbacks callbacks;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callbacks = (AdapterFragmentCallbacks) activity;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
}
}
@@ -1,8 +0,0 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -1,5 +0,0 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -1,55 +0,0 @@
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);
}
}
@@ -1,86 +0,0 @@
package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static long getFileSize(File root, String... path) {
return openPath(false, root, path).length();
}
public static boolean deleteCacheFile(File root, String... path) {
return openPath(false, root, path).delete();
}
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
}
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
}
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
}
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out, long maxLength) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
maxLength -= bytesRead;
if (maxLength <= 0) {
throw new IOException("Stream exceeded max size");
}
out.write(buf, 0, bytesRead);
}
}
public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
char[] buf = new char[256];
int bytesRead;
while ((bytesRead = r.read(buf)) != -1) {
sb.append(buf, 0, bytesRead);
}
try {
in.close();
} catch (IOException ignored) {}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,71 +7,70 @@ import android.app.AlertDialog;
import android.content.DialogInterface;
public class Dialog implements Runnable {
private final String title;
private final String message;
private final Activity activity;
private final boolean endAfterDismiss;
private String title, message;
private Activity activity;
private boolean endAfterDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
}
});
synchronized (rundownDialogs) {
rundownDialogs.add(this);
alert.show();
}
}
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
}
});
synchronized (rundownDialogs) {
rundownDialogs.add(this);
alert.show();
}
}
}
@@ -1,90 +0,0 @@
package com.limelight.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
parent.startActivity(intent);
}
public static void doQuit(final Activity parent,
final InetAddress address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
}
else {
message = e.getMessage();
}
} catch (UnknownHostException e) {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
if (onComplete != null) {
onComplete.run();
}
}
final String toastMessage = message;
parent.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
}
@@ -9,112 +9,111 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener {
private final String title;
private final String message;
private final Activity activity;
private ProgressDialog progress;
private final boolean finish;
private String title, message;
private Activity activity;
private ProgressDialog progress;
private boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
this.activity = activity;
this.title = title;
this.message = message;
this.progress = null;
this.finish = finish;
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
// If we're dying, don't bother doing anything
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss();
}
}
}
}
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
this.activity = activity;
this.title = title;
this.message = message;
this.progress = null;
this.finish = finish;
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
// If we're dying, don't bother doing anything
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss();
}
}
}
}
@Override
public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
}
// This will only be called if finish was true, so we don't need to check again
activity.finish();
}
@Override
public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
}
// This will only be called if finish was true, so we don't need to check again
activity.finish();
}
}
@@ -1,62 +0,0 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.view.View;
import com.limelight.R;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void notifyNewRootView(Activity activity)
{
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}
+1 -2
View File
@@ -3,8 +3,7 @@
# Our minimum version is Android 4.1
APP_PLATFORM := android-16
# Support all modern ABIs
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
# We want an optimized build
APP_OPTIM := release
+1 -20
View File
@@ -10,23 +10,4 @@ LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# This next portion of the makefile is mostly copied from build-executable.mk but
# creates a binary with the libXXX.so form so the APK will install and drop
# the binary correctly.
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
# we are building target objects
my := TARGET_
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
include $(BUILD_SHARED_LIBRARY)
+80 -372
View File
@@ -1,410 +1,118 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <jni.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>
#include <dirent.h>
#include <pthread.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <android/log.h>
#define EVDEV_MAX_EVENT_SIZE 24
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
const char *path;
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
return open(path, O_RDWR);
}
#define REL_X 0x00
#define REL_Y 0x01
#define KEY_Q 16
#define BTN_LEFT 0x110
#define BTN_GAMEPAD 0x130
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 1) == 0;
}
struct DeviceEntry {
struct DeviceEntry *next;
pthread_t thread;
int fd;
char devName[128];
};
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 0) == 0;
}
static struct DeviceEntry *DeviceListHead;
static int grabbing = 1;
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t SocketSendLock = PTHREAD_MUTEX_INITIALIZER;
static int sock;
// This is a small executable that runs in a root shell. It reads input
// devices and writes the evdev output packets to a socket. This allows
// Moonlight to read input devices without having to muck with changing
// device permissions or modifying SELinux policy (which is prevented in
// Marshmallow anyway).
// has*() and friends are based on Android's EventHub.cpp
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
static int hasRelAxis(int fd, short axis) {
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
unsigned char relBitmask[(REL_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
return test_bit(axis, relBitmask);
}
static int hasKey(int fd, short key) {
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
unsigned char absBitmask[(ABS_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
return test_bit(axis, absBitmask);
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
return test_bit(key, keyBitmask);
}
static void outputEvdevData(char *data, int dataSize) {
char packetBuffer[EVDEV_MAX_EVENT_SIZE + sizeof(dataSize)];
// Copy the full packet into our buffer
memcpy(packetBuffer, &dataSize, sizeof(dataSize));
memcpy(&packetBuffer[sizeof(dataSize)], data, dataSize);
// Lock to prevent other threads from sending at the same time
pthread_mutex_lock(&SocketSendLock);
send(sock, packetBuffer, dataSize + sizeof(dataSize), 0);
pthread_mutex_unlock(&SocketSendLock);
}
void* pollThreadFunc(void* context) {
struct DeviceEntry *device = context;
struct pollfd pollinfo;
int pollres, ret;
char data[EVDEV_MAX_EVENT_SIZE];
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
if (grabbing) {
// Exclusively grab the input device (required to make the Android cursor disappear)
if (ioctl(device->fd, EVIOCGRAB, 1) < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"EVIOCGRAB failed for %s: %d", device->devName, errno);
goto cleanup;
}
}
for (;;) {
do {
// Unwait every 250 ms to return to caller if the fd is closed
pollinfo.fd = device->fd;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 250);
}
while (pollres == 0);
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() failed: %d", errno);
goto cleanup;
}
else if (ret == 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() graceful EOF");
goto cleanup;
}
else if (grabbing) {
// Write out the data to our client
outputEvdevData(data, ret);
}
}
else {
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"poll() failed: %d", errno);
}
else {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Unexpected revents: %d", pollinfo.revents);
}
// Terminate this thread
goto cleanup;
}
}
cleanup:
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Closing /dev/input/%s", device->devName);
// Remove the context from the linked list
{
struct DeviceEntry *lastEntry;
// Lock the device list
pthread_mutex_lock(&DeviceListLock);
if (DeviceListHead == device) {
DeviceListHead = device->next;
}
else {
lastEntry = DeviceListHead;
while (lastEntry->next != NULL) {
if (lastEntry->next == device) {
lastEntry->next = device->next;
break;
}
lastEntry = lastEntry->next;
}
}
// Unlock device list
pthread_mutex_unlock(&DeviceListLock);
}
// Free the context
ioctl(device->fd, EVIOCGRAB, 0);
close(device->fd);
free(device);
return NULL;
}
static int precheckDeviceForPolling(int fd) {
int isMouse;
int isKeyboard;
int isGamepad;
// This is the same check that Android does in EventHub.cpp
isMouse = hasRelAxis(fd, REL_X) &&
hasRelAxis(fd, REL_Y) &&
hasKey(fd, BTN_LEFT);
// This is the same check that Android does in EventHub.cpp
isKeyboard = hasKey(fd, KEY_Q);
isGamepad = hasKey(fd, BTN_GAMEPAD);
// We only handle keyboards and mice that aren't gamepads
return (isMouse || isKeyboard) && !isGamepad;
}
static void startPollForDevice(char* deviceName) {
struct DeviceEntry *currentEntry;
char fullPath[256];
int fd;
// Lock the device list
pthread_mutex_lock(&DeviceListLock);
// Check if the device is already being polled
currentEntry = DeviceListHead;
while (currentEntry != NULL) {
if (strcmp(currentEntry->devName, deviceName) == 0) {
// Already polling this device
goto unlock;
}
currentEntry = currentEntry->next;
}
// Open the device
sprintf(fullPath, "/dev/input/%s", deviceName);
fd = open(fullPath, O_RDWR);
if (fd < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open %s: %d", fullPath, errno);
goto unlock;
}
// Allocate a context
currentEntry = malloc(sizeof(*currentEntry));
if (currentEntry == NULL) {
close(fd);
goto unlock;
}
// Populate context
currentEntry->fd = fd;
strcpy(currentEntry->devName, deviceName);
// Check if we support polling this device
if (!precheckDeviceForPolling(fd)) {
// Nope, get out
free(currentEntry);
close(fd);
goto unlock;
}
// Start the polling thread
if (pthread_create(&currentEntry->thread, NULL, pollThreadFunc, currentEntry) != 0) {
free(currentEntry);
close(fd);
goto unlock;
}
// Queue this onto the device list
currentEntry->next = DeviceListHead;
DeviceListHead = currentEntry;
unlock:
// Unlock and return
pthread_mutex_unlock(&DeviceListLock);
}
static int enumerateDevices(void) {
DIR *inputDir;
struct dirent *dirEnt;
inputDir = opendir("/dev/input");
if (!inputDir) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open /dev/input: %d", errno);
return -1;
}
// Start polling each device in /dev/input
while ((dirEnt = readdir(inputDir)) != NULL) {
if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0) {
// Skip these virtual directories
continue;
}
if (strstr(dirEnt->d_name, "event") == NULL) {
// Skip non-event devices
continue;
}
startPollForDevice(dirEnt->d_name);
}
closedir(inputDir);
return 0;
}
static int connectSocket(int port) {
struct sockaddr_in saddr;
int ret;
int val;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "socket() failed: %d", errno);
return -1;
}
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ret = connect(sock, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "connect() failed: %d", errno);
return -1;
}
val = 1;
ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "setsockopt() failed: %d", errno);
// We can continue anyways
}
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Connection established to port %d", port);
return 0;
}
#define UNGRAB_REQ 1
#define REGRAB_REQ 2
int main(int argc, char* argv[]) {
int ret;
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
jint ret;
jbyte *data;
int pollres;
struct pollfd pollinfo;
int port;
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
port = atoi(argv[1]);
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
// Connect to the app's socket
ret = connectSocket(port);
if (ret < 0) {
return ret;
data = (*env)->GetByteArrayElements(env, buffer, NULL);
if (data == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Failed to get byte array");
return -1;
}
// Perform initial enumeration
ret = enumerateDevices();
if (ret < 0) {
return ret;
do
{
// Unwait every 250 ms to return to caller if the fd is closed
pollinfo.fd = fd;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 250);
}
// Wait for requests from the client
for (;;) {
unsigned char requestId;
do {
// Every second we poll again for new devices if
// we haven't received any new events
pollinfo.fd = sock;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 1000);
if (pollres == 0) {
// Timeout, re-enumerate devices
enumerateDevices();
}
while (pollres == 0);
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = read(fd, data, sizeof(struct input_event));
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() failed: %d", errno);
}
while (pollres == 0);
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = recv(sock, &requestId, sizeof(requestId), 0);
if (ret < sizeof(requestId)) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
return errno;
}
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
return requestId;
}
{
struct DeviceEntry *currentEntry;
pthread_mutex_lock(&DeviceListLock);
// Update state for future devices
grabbing = (requestId == REGRAB_REQ);
// Carry out the requested action on each device
currentEntry = DeviceListHead;
while (currentEntry != NULL) {
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
currentEntry = currentEntry->next;
}
pthread_mutex_unlock(&DeviceListLock);
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "New grab status is: %s",
grabbing ? "enabled" : "disabled");
}
}
else {
// There must have been a failure
ret = -1;
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"poll() failed: %d", errno);
}
else {
// Terminate this thread
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Socket recv poll() failed: %d", errno);
}
else {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Socket poll unexpected revents: %d", pollinfo.revents);
}
return -1;
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Unexpected revents: %d", pollinfo.revents);
}
}
}
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
return ret;
}
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
return close(fd);
}
-25
View File
@@ -1,25 +0,0 @@
# 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)
-148
View File
@@ -1,148 +0,0 @@
#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);
}
+17
View File
@@ -0,0 +1,17 @@
# Android.mk for Limelight's H264 decoder
MY_LOCAL_PATH := $(call my-dir)
include $(call all-subdir-makefiles)
LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := nv_avc_dec
LOCAL_SRC_FILES := nv_avc_dec.c nv_avc_dec_jni.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/$(TARGET_ARCH_ABI)/include
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
# Link to ffmpeg libraries
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libwsresample
include $(BUILD_SHARED_LIBRARY)
@@ -0,0 +1,31 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavcodec-55.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavformat-55.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswscale-2.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavutil-52.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libwsresample
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,116 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AVFFT_H
#define AVCODEC_AVFFT_H
/**
* @file
* @ingroup lavc_fft
* FFT functions
*/
/**
* @defgroup lavc_fft FFT functions
* @ingroup lavc_misc
*
* @{
*/
typedef float FFTSample;
typedef struct FFTComplex {
FFTSample re, im;
} FFTComplex;
typedef struct FFTContext FFTContext;
/**
* Set up a complex FFT.
* @param nbits log2 of the length of the input array
* @param inverse if 0 perform the forward transform, if 1 perform the inverse
*/
FFTContext *av_fft_init(int nbits, int inverse);
/**
* Do the permutation needed BEFORE calling ff_fft_calc().
*/
void av_fft_permute(FFTContext *s, FFTComplex *z);
/**
* Do a complex FFT with the parameters defined in av_fft_init(). The
* input data must be permuted before. No 1.0/sqrt(n) normalization is done.
*/
void av_fft_calc(FFTContext *s, FFTComplex *z);
void av_fft_end(FFTContext *s);
FFTContext *av_mdct_init(int nbits, int inverse, double scale);
void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
void av_mdct_end(FFTContext *s);
/* Real Discrete Fourier Transform */
enum RDFTransformType {
DFT_R2C,
IDFT_C2R,
IDFT_R2C,
DFT_C2R,
};
typedef struct RDFTContext RDFTContext;
/**
* Set up a real FFT.
* @param nbits log2 of the length of the input array
* @param trans the type of transform
*/
RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
void av_rdft_calc(RDFTContext *s, FFTSample *data);
void av_rdft_end(RDFTContext *s);
/* Discrete Cosine Transform */
typedef struct DCTContext DCTContext;
enum DCTTransformType {
DCT_II = 0,
DCT_III,
DCT_I,
DST_I,
};
/**
* Set up DCT.
* @param nbits size of the input array:
* (1 << nbits) for DCT-II, DCT-III and DST-I
* (1 << nbits) + 1 for DCT-I
*
* @note the first element of the input of DST-I is ignored
*/
DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
void av_dct_calc(DCTContext *s, FFTSample *data);
void av_dct_end (DCTContext *s);
/**
* @}
*/
#endif /* AVCODEC_AVFFT_H */
@@ -0,0 +1,95 @@
/*
* DXVA2 HW acceleration
*
* copyright (c) 2009 Laurent Aimar
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_DXVA_H
#define AVCODEC_DXVA_H
/**
* @file
* @ingroup lavc_codec_hwaccel_dxva2
* Public libavcodec DXVA2 header.
*/
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0600
#undef _WIN32_WINNT
#endif
#if !defined(_WIN32_WINNT)
#define _WIN32_WINNT 0x0600
#endif
#include <stdint.h>
#include <d3d9.h>
#include <dxva2api.h>
/**
* @defgroup lavc_codec_hwaccel_dxva2 DXVA2
* @ingroup lavc_codec_hwaccel
*
* @{
*/
#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
/**
* This structure is used to provides the necessary configurations and data
* to the DXVA2 FFmpeg HWAccel implementation.
*
* The application must make it available as AVCodecContext.hwaccel_context.
*/
struct dxva_context {
/**
* DXVA2 decoder object
*/
IDirectXVideoDecoder *decoder;
/**
* DXVA2 configuration used to create the decoder
*/
const DXVA2_ConfigPictureDecode *cfg;
/**
* The number of surface in the surface array
*/
unsigned surface_count;
/**
* The array of Direct3D surfaces used to create the decoder
*/
LPDIRECT3DSURFACE9 *surface;
/**
* A bit field configuring the workarounds needed for using the decoder
*/
uint64_t workaround;
/**
* Private to the FFmpeg AVHWAccel implementation
*/
unsigned report_id;
};
/**
* @}
*/
#endif /* AVCODEC_DXVA_H */
@@ -0,0 +1,397 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_OLD_CODEC_IDS_H
#define AVCODEC_OLD_CODEC_IDS_H
#include "libavutil/common.h"
/*
* This header exists to prevent new codec IDs from being accidentally added to
* the deprecated list.
* Do not include it directly. It will be removed on next major bump
*
* Do not add new items to this list. Use the AVCodecID enum instead.
*/
CODEC_ID_NONE = AV_CODEC_ID_NONE,
/* video codecs */
CODEC_ID_MPEG1VIDEO,
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
CODEC_ID_MPEG2VIDEO_XVMC,
CODEC_ID_H261,
CODEC_ID_H263,
CODEC_ID_RV10,
CODEC_ID_RV20,
CODEC_ID_MJPEG,
CODEC_ID_MJPEGB,
CODEC_ID_LJPEG,
CODEC_ID_SP5X,
CODEC_ID_JPEGLS,
CODEC_ID_MPEG4,
CODEC_ID_RAWVIDEO,
CODEC_ID_MSMPEG4V1,
CODEC_ID_MSMPEG4V2,
CODEC_ID_MSMPEG4V3,
CODEC_ID_WMV1,
CODEC_ID_WMV2,
CODEC_ID_H263P,
CODEC_ID_H263I,
CODEC_ID_FLV1,
CODEC_ID_SVQ1,
CODEC_ID_SVQ3,
CODEC_ID_DVVIDEO,
CODEC_ID_HUFFYUV,
CODEC_ID_CYUV,
CODEC_ID_H264,
CODEC_ID_INDEO3,
CODEC_ID_VP3,
CODEC_ID_THEORA,
CODEC_ID_ASV1,
CODEC_ID_ASV2,
CODEC_ID_FFV1,
CODEC_ID_4XM,
CODEC_ID_VCR1,
CODEC_ID_CLJR,
CODEC_ID_MDEC,
CODEC_ID_ROQ,
CODEC_ID_INTERPLAY_VIDEO,
CODEC_ID_XAN_WC3,
CODEC_ID_XAN_WC4,
CODEC_ID_RPZA,
CODEC_ID_CINEPAK,
CODEC_ID_WS_VQA,
CODEC_ID_MSRLE,
CODEC_ID_MSVIDEO1,
CODEC_ID_IDCIN,
CODEC_ID_8BPS,
CODEC_ID_SMC,
CODEC_ID_FLIC,
CODEC_ID_TRUEMOTION1,
CODEC_ID_VMDVIDEO,
CODEC_ID_MSZH,
CODEC_ID_ZLIB,
CODEC_ID_QTRLE,
CODEC_ID_TSCC,
CODEC_ID_ULTI,
CODEC_ID_QDRAW,
CODEC_ID_VIXL,
CODEC_ID_QPEG,
CODEC_ID_PNG,
CODEC_ID_PPM,
CODEC_ID_PBM,
CODEC_ID_PGM,
CODEC_ID_PGMYUV,
CODEC_ID_PAM,
CODEC_ID_FFVHUFF,
CODEC_ID_RV30,
CODEC_ID_RV40,
CODEC_ID_VC1,
CODEC_ID_WMV3,
CODEC_ID_LOCO,
CODEC_ID_WNV1,
CODEC_ID_AASC,
CODEC_ID_INDEO2,
CODEC_ID_FRAPS,
CODEC_ID_TRUEMOTION2,
CODEC_ID_BMP,
CODEC_ID_CSCD,
CODEC_ID_MMVIDEO,
CODEC_ID_ZMBV,
CODEC_ID_AVS,
CODEC_ID_SMACKVIDEO,
CODEC_ID_NUV,
CODEC_ID_KMVC,
CODEC_ID_FLASHSV,
CODEC_ID_CAVS,
CODEC_ID_JPEG2000,
CODEC_ID_VMNC,
CODEC_ID_VP5,
CODEC_ID_VP6,
CODEC_ID_VP6F,
CODEC_ID_TARGA,
CODEC_ID_DSICINVIDEO,
CODEC_ID_TIERTEXSEQVIDEO,
CODEC_ID_TIFF,
CODEC_ID_GIF,
CODEC_ID_DXA,
CODEC_ID_DNXHD,
CODEC_ID_THP,
CODEC_ID_SGI,
CODEC_ID_C93,
CODEC_ID_BETHSOFTVID,
CODEC_ID_PTX,
CODEC_ID_TXD,
CODEC_ID_VP6A,
CODEC_ID_AMV,
CODEC_ID_VB,
CODEC_ID_PCX,
CODEC_ID_SUNRAST,
CODEC_ID_INDEO4,
CODEC_ID_INDEO5,
CODEC_ID_MIMIC,
CODEC_ID_RL2,
CODEC_ID_ESCAPE124,
CODEC_ID_DIRAC,
CODEC_ID_BFI,
CODEC_ID_CMV,
CODEC_ID_MOTIONPIXELS,
CODEC_ID_TGV,
CODEC_ID_TGQ,
CODEC_ID_TQI,
CODEC_ID_AURA,
CODEC_ID_AURA2,
CODEC_ID_V210X,
CODEC_ID_TMV,
CODEC_ID_V210,
CODEC_ID_DPX,
CODEC_ID_MAD,
CODEC_ID_FRWU,
CODEC_ID_FLASHSV2,
CODEC_ID_CDGRAPHICS,
CODEC_ID_R210,
CODEC_ID_ANM,
CODEC_ID_BINKVIDEO,
CODEC_ID_IFF_ILBM,
CODEC_ID_IFF_BYTERUN1,
CODEC_ID_KGV1,
CODEC_ID_YOP,
CODEC_ID_VP8,
CODEC_ID_PICTOR,
CODEC_ID_ANSI,
CODEC_ID_A64_MULTI,
CODEC_ID_A64_MULTI5,
CODEC_ID_R10K,
CODEC_ID_MXPEG,
CODEC_ID_LAGARITH,
CODEC_ID_PRORES,
CODEC_ID_JV,
CODEC_ID_DFA,
CODEC_ID_WMV3IMAGE,
CODEC_ID_VC1IMAGE,
CODEC_ID_UTVIDEO,
CODEC_ID_BMV_VIDEO,
CODEC_ID_VBLE,
CODEC_ID_DXTORY,
CODEC_ID_V410,
CODEC_ID_XWD,
CODEC_ID_CDXL,
CODEC_ID_XBM,
CODEC_ID_ZEROCODEC,
CODEC_ID_MSS1,
CODEC_ID_MSA1,
CODEC_ID_TSCC2,
CODEC_ID_MTS2,
CODEC_ID_CLLC,
CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
CODEC_ID_ESCAPE130 = MKBETAG('E','1','3','0'),
CODEC_ID_EXR = MKBETAG('0','E','X','R'),
CODEC_ID_AVRP = MKBETAG('A','V','R','P'),
CODEC_ID_G2M = MKBETAG( 0 ,'G','2','M'),
CODEC_ID_AVUI = MKBETAG('A','V','U','I'),
CODEC_ID_AYUV = MKBETAG('A','Y','U','V'),
CODEC_ID_V308 = MKBETAG('V','3','0','8'),
CODEC_ID_V408 = MKBETAG('V','4','0','8'),
CODEC_ID_YUV4 = MKBETAG('Y','U','V','4'),
CODEC_ID_SANM = MKBETAG('S','A','N','M'),
CODEC_ID_PAF_VIDEO = MKBETAG('P','A','F','V'),
CODEC_ID_SNOW = AV_CODEC_ID_SNOW,
/* various PCM "codecs" */
CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
CODEC_ID_PCM_S16LE = 0x10000,
CODEC_ID_PCM_S16BE,
CODEC_ID_PCM_U16LE,
CODEC_ID_PCM_U16BE,
CODEC_ID_PCM_S8,
CODEC_ID_PCM_U8,
CODEC_ID_PCM_MULAW,
CODEC_ID_PCM_ALAW,
CODEC_ID_PCM_S32LE,
CODEC_ID_PCM_S32BE,
CODEC_ID_PCM_U32LE,
CODEC_ID_PCM_U32BE,
CODEC_ID_PCM_S24LE,
CODEC_ID_PCM_S24BE,
CODEC_ID_PCM_U24LE,
CODEC_ID_PCM_U24BE,
CODEC_ID_PCM_S24DAUD,
CODEC_ID_PCM_ZORK,
CODEC_ID_PCM_S16LE_PLANAR,
CODEC_ID_PCM_DVD,
CODEC_ID_PCM_F32BE,
CODEC_ID_PCM_F32LE,
CODEC_ID_PCM_F64BE,
CODEC_ID_PCM_F64LE,
CODEC_ID_PCM_BLURAY,
CODEC_ID_PCM_LXF,
CODEC_ID_S302M,
CODEC_ID_PCM_S8_PLANAR,
/* various ADPCM codecs */
CODEC_ID_ADPCM_IMA_QT = 0x11000,
CODEC_ID_ADPCM_IMA_WAV,
CODEC_ID_ADPCM_IMA_DK3,
CODEC_ID_ADPCM_IMA_DK4,
CODEC_ID_ADPCM_IMA_WS,
CODEC_ID_ADPCM_IMA_SMJPEG,
CODEC_ID_ADPCM_MS,
CODEC_ID_ADPCM_4XM,
CODEC_ID_ADPCM_XA,
CODEC_ID_ADPCM_ADX,
CODEC_ID_ADPCM_EA,
CODEC_ID_ADPCM_G726,
CODEC_ID_ADPCM_CT,
CODEC_ID_ADPCM_SWF,
CODEC_ID_ADPCM_YAMAHA,
CODEC_ID_ADPCM_SBPRO_4,
CODEC_ID_ADPCM_SBPRO_3,
CODEC_ID_ADPCM_SBPRO_2,
CODEC_ID_ADPCM_THP,
CODEC_ID_ADPCM_IMA_AMV,
CODEC_ID_ADPCM_EA_R1,
CODEC_ID_ADPCM_EA_R3,
CODEC_ID_ADPCM_EA_R2,
CODEC_ID_ADPCM_IMA_EA_SEAD,
CODEC_ID_ADPCM_IMA_EA_EACS,
CODEC_ID_ADPCM_EA_XAS,
CODEC_ID_ADPCM_EA_MAXIS_XA,
CODEC_ID_ADPCM_IMA_ISS,
CODEC_ID_ADPCM_G722,
CODEC_ID_ADPCM_IMA_APC,
CODEC_ID_VIMA = MKBETAG('V','I','M','A'),
/* AMR */
CODEC_ID_AMR_NB = 0x12000,
CODEC_ID_AMR_WB,
/* RealAudio codecs*/
CODEC_ID_RA_144 = 0x13000,
CODEC_ID_RA_288,
/* various DPCM codecs */
CODEC_ID_ROQ_DPCM = 0x14000,
CODEC_ID_INTERPLAY_DPCM,
CODEC_ID_XAN_DPCM,
CODEC_ID_SOL_DPCM,
/* audio codecs */
CODEC_ID_MP2 = 0x15000,
CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
CODEC_ID_AAC,
CODEC_ID_AC3,
CODEC_ID_DTS,
CODEC_ID_VORBIS,
CODEC_ID_DVAUDIO,
CODEC_ID_WMAV1,
CODEC_ID_WMAV2,
CODEC_ID_MACE3,
CODEC_ID_MACE6,
CODEC_ID_VMDAUDIO,
CODEC_ID_FLAC,
CODEC_ID_MP3ADU,
CODEC_ID_MP3ON4,
CODEC_ID_SHORTEN,
CODEC_ID_ALAC,
CODEC_ID_WESTWOOD_SND1,
CODEC_ID_GSM, ///< as in Berlin toast format
CODEC_ID_QDM2,
CODEC_ID_COOK,
CODEC_ID_TRUESPEECH,
CODEC_ID_TTA,
CODEC_ID_SMACKAUDIO,
CODEC_ID_QCELP,
CODEC_ID_WAVPACK,
CODEC_ID_DSICINAUDIO,
CODEC_ID_IMC,
CODEC_ID_MUSEPACK7,
CODEC_ID_MLP,
CODEC_ID_GSM_MS, /* as found in WAV */
CODEC_ID_ATRAC3,
CODEC_ID_VOXWARE,
CODEC_ID_APE,
CODEC_ID_NELLYMOSER,
CODEC_ID_MUSEPACK8,
CODEC_ID_SPEEX,
CODEC_ID_WMAVOICE,
CODEC_ID_WMAPRO,
CODEC_ID_WMALOSSLESS,
CODEC_ID_ATRAC3P,
CODEC_ID_EAC3,
CODEC_ID_SIPR,
CODEC_ID_MP1,
CODEC_ID_TWINVQ,
CODEC_ID_TRUEHD,
CODEC_ID_MP4ALS,
CODEC_ID_ATRAC1,
CODEC_ID_BINKAUDIO_RDFT,
CODEC_ID_BINKAUDIO_DCT,
CODEC_ID_AAC_LATM,
CODEC_ID_QDMC,
CODEC_ID_CELT,
CODEC_ID_G723_1,
CODEC_ID_G729,
CODEC_ID_8SVX_EXP,
CODEC_ID_8SVX_FIB,
CODEC_ID_BMV_AUDIO,
CODEC_ID_RALF,
CODEC_ID_IAC,
CODEC_ID_ILBC,
CODEC_ID_FFWAVESYNTH = MKBETAG('F','F','W','S'),
CODEC_ID_SONIC = MKBETAG('S','O','N','C'),
CODEC_ID_SONIC_LS = MKBETAG('S','O','N','L'),
CODEC_ID_PAF_AUDIO = MKBETAG('P','A','F','A'),
CODEC_ID_OPUS = MKBETAG('O','P','U','S'),
/* subtitle codecs */
CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
CODEC_ID_DVD_SUBTITLE = 0x17000,
CODEC_ID_DVB_SUBTITLE,
CODEC_ID_TEXT, ///< raw UTF-8 text
CODEC_ID_XSUB,
CODEC_ID_SSA,
CODEC_ID_MOV_TEXT,
CODEC_ID_HDMV_PGS_SUBTITLE,
CODEC_ID_DVB_TELETEXT,
CODEC_ID_SRT,
CODEC_ID_MICRODVD = MKBETAG('m','D','V','D'),
CODEC_ID_EIA_608 = MKBETAG('c','6','0','8'),
CODEC_ID_JACOSUB = MKBETAG('J','S','U','B'),
CODEC_ID_SAMI = MKBETAG('S','A','M','I'),
CODEC_ID_REALTEXT = MKBETAG('R','T','X','T'),
CODEC_ID_SUBVIEWER = MKBETAG('S','u','b','V'),
/* other specific kind of codecs (generally used for attachments) */
CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
CODEC_ID_TTF = 0x18000,
CODEC_ID_BINTEXT = MKBETAG('B','T','X','T'),
CODEC_ID_XBIN = MKBETAG('X','B','I','N'),
CODEC_ID_IDF = MKBETAG( 0 ,'I','D','F'),
CODEC_ID_OTF = MKBETAG( 0 ,'O','T','F'),
CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
* stream (only used by libavformat) */
CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
* stream (only used by libavformat) */
CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
#endif /* AVCODEC_OLD_CODEC_IDS_H */
@@ -0,0 +1,173 @@
/*
* Video Acceleration API (shared data between FFmpeg and the video player)
* HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
*
* Copyright (C) 2008-2009 Splitted-Desktop Systems
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VAAPI_H
#define AVCODEC_VAAPI_H
/**
* @file
* @ingroup lavc_codec_hwaccel_vaapi
* Public libavcodec VA API header.
*/
#include <stdint.h>
/**
* @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
* @ingroup lavc_codec_hwaccel
* @{
*/
/**
* This structure is used to share data between the FFmpeg library and
* the client video application.
* This shall be zero-allocated and available as
* AVCodecContext.hwaccel_context. All user members can be set once
* during initialization or through each AVCodecContext.get_buffer()
* function call. In any case, they must be valid prior to calling
* decoding functions.
*/
struct vaapi_context {
/**
* Window system dependent data
*
* - encoding: unused
* - decoding: Set by user
*/
void *display;
/**
* Configuration ID
*
* - encoding: unused
* - decoding: Set by user
*/
uint32_t config_id;
/**
* Context ID (video decode pipeline)
*
* - encoding: unused
* - decoding: Set by user
*/
uint32_t context_id;
/**
* VAPictureParameterBuffer ID
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
uint32_t pic_param_buf_id;
/**
* VAIQMatrixBuffer ID
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
uint32_t iq_matrix_buf_id;
/**
* VABitPlaneBuffer ID (for VC-1 decoding)
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
uint32_t bitplane_buf_id;
/**
* Slice parameter/data buffer IDs
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
uint32_t *slice_buf_ids;
/**
* Number of effective slice buffer IDs to send to the HW
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
unsigned int n_slice_buf_ids;
/**
* Size of pre-allocated slice_buf_ids
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
unsigned int slice_buf_ids_alloc;
/**
* Pointer to VASliceParameterBuffers
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
void *slice_params;
/**
* Size of a VASliceParameterBuffer element
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
unsigned int slice_param_size;
/**
* Size of pre-allocated slice_params
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
unsigned int slice_params_alloc;
/**
* Number of slices currently filled in
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
unsigned int slice_count;
/**
* Pointer to slice data buffer base
* - encoding: unused
* - decoding: Set by libavcodec
*/
const uint8_t *slice_data;
/**
* Current size of slice data
*
* - encoding: unused
* - decoding: Set by libavcodec
*/
uint32_t slice_data_size;
};
/* @} */
#endif /* AVCODEC_VAAPI_H */
@@ -0,0 +1,162 @@
/*
* VDA HW acceleration
*
* copyright (c) 2011 Sebastien Zwickert
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VDA_H
#define AVCODEC_VDA_H
/**
* @file
* @ingroup lavc_codec_hwaccel_vda
* Public libavcodec VDA header.
*/
#include <stdint.h>
// emmintrin.h is unable to compile with -std=c99 -Werror=missing-prototypes
// http://openradar.appspot.com/8026390
#undef __GNUC_STDC_INLINE__
#define Picture QuickdrawPicture
#include <VideoDecodeAcceleration/VDADecoder.h>
#undef Picture
#include "libavcodec/version.h"
/**
* @defgroup lavc_codec_hwaccel_vda VDA
* @ingroup lavc_codec_hwaccel
*
* @{
*/
/**
* This structure is used to provide the necessary configurations and data
* to the VDA FFmpeg HWAccel implementation.
*
* The application must make it available as AVCodecContext.hwaccel_context.
*/
struct vda_context {
/**
* VDA decoder object.
*
* - encoding: unused
* - decoding: Set/Unset by libavcodec.
*/
VDADecoder decoder;
/**
* The Core Video pixel buffer that contains the current image data.
*
* encoding: unused
* decoding: Set by libavcodec. Unset by user.
*/
CVPixelBufferRef cv_buffer;
/**
* Use the hardware decoder in synchronous mode.
*
* encoding: unused
* decoding: Set by user.
*/
int use_sync_decoding;
/**
* The frame width.
*
* - encoding: unused
* - decoding: Set/Unset by user.
*/
int width;
/**
* The frame height.
*
* - encoding: unused
* - decoding: Set/Unset by user.
*/
int height;
/**
* The frame format.
*
* - encoding: unused
* - decoding: Set/Unset by user.
*/
int format;
/**
* The pixel format for output image buffers.
*
* - encoding: unused
* - decoding: Set/Unset by user.
*/
OSType cv_pix_fmt_type;
/**
* The current bitstream buffer.
*
* - encoding: unused
* - decoding: Set/Unset by libavcodec.
*/
uint8_t *priv_bitstream;
/**
* The current size of the bitstream.
*
* - encoding: unused
* - decoding: Set/Unset by libavcodec.
*/
int priv_bitstream_size;
/**
* The reference size used for fast reallocation.
*
* - encoding: unused
* - decoding: Set/Unset by libavcodec.
*/
int priv_allocated_size;
/**
* Use av_buffer to manage buffer.
* When the flag is set, the CVPixelBuffers returned by the decoder will
* be released automatically, so you have to retain them if necessary.
* Not setting this flag may cause memory leak.
*
* encoding: unused
* decoding: Set by user.
*/
int use_ref_buffer;
};
/** Create the video decoder. */
int ff_vda_create_decoder(struct vda_context *vda_ctx,
uint8_t *extradata,
int extradata_size);
/** Destroy the video decoder. */
int ff_vda_destroy_decoder(struct vda_context *vda_ctx);
/**
* @}
*/
#endif /* AVCODEC_VDA_H */
@@ -0,0 +1,195 @@
/*
* The Video Decode and Presentation API for UNIX (VDPAU) is used for
* hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
*
* Copyright (C) 2008 NVIDIA
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VDPAU_H
#define AVCODEC_VDPAU_H
/**
* @file
* @ingroup lavc_codec_hwaccel_vdpau
* Public libavcodec VDPAU header.
*/
/**
* @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
* @ingroup lavc_codec_hwaccel
*
* VDPAU hardware acceleration has two modules
* - VDPAU decoding
* - VDPAU presentation
*
* The VDPAU decoding module parses all headers using FFmpeg
* parsing mechanisms and uses VDPAU for the actual decoding.
*
* As per the current implementation, the actual decoding
* and rendering (API calls) are done as part of the VDPAU
* presentation (vo_vdpau.c) module.
*
* @{
*/
#include <vdpau/vdpau.h>
#include <vdpau/vdpau_x11.h>
#include "libavutil/avconfig.h"
#include "libavutil/attributes.h"
#ifndef FF_API_CAP_VDPAU
#define FF_API_CAP_VDPAU 1
#endif
#ifndef FF_API_BUFS_VDPAU
#define FF_API_BUFS_VDPAU 1
#endif
#if FF_API_BUFS_VDPAU
union AVVDPAUPictureInfo {
VdpPictureInfoH264 h264;
VdpPictureInfoMPEG1Or2 mpeg;
VdpPictureInfoVC1 vc1;
VdpPictureInfoMPEG4Part2 mpeg4;
};
#endif
struct AVCodecContext;
struct AVFrame;
typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *,
const VdpPictureInfo *, uint32_t,
const VdpBitstreamBuffer *);
/**
* This structure is used to share data between the libavcodec library and
* the client video application.
* The user shall allocate the structure via the av_alloc_vdpau_hwaccel
* function and make it available as
* AVCodecContext.hwaccel_context. Members can be set by the user once
* during initialization or through each AVCodecContext.get_buffer()
* function call. In any case, they must be valid prior to calling
* decoding functions.
*/
typedef struct AVVDPAUContext {
/**
* VDPAU decoder handle
*
* Set by user.
*/
VdpDecoder decoder;
/**
* VDPAU decoder render callback
*
* Set by the user.
*/
VdpDecoderRender *render;
#if FF_API_BUFS_VDPAU
/**
* VDPAU picture information
*
* Set by libavcodec.
*/
attribute_deprecated
union AVVDPAUPictureInfo info;
/**
* Allocated size of the bitstream_buffers table.
*
* Set by libavcodec.
*/
attribute_deprecated
int bitstream_buffers_allocated;
/**
* Useful bitstream buffers in the bitstream buffers table.
*
* Set by libavcodec.
*/
attribute_deprecated
int bitstream_buffers_used;
/**
* Table of bitstream buffers.
* The user is responsible for freeing this buffer using av_freep().
*
* Set by libavcodec.
*/
attribute_deprecated
VdpBitstreamBuffer *bitstream_buffers;
#endif
AVVDPAU_Render2 render2;
} AVVDPAUContext;
/**
* @brief allocation function for AVVDPAUContext
*
* Allows extending the struct without breaking API/ABI
*/
AVVDPAUContext *av_alloc_vdpaucontext(void);
AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
#if FF_API_CAP_VDPAU
/** @brief The videoSurface is used for rendering. */
#define FF_VDPAU_STATE_USED_FOR_RENDER 1
/**
* @brief The videoSurface is needed for reference/prediction.
* The codec manipulates this.
*/
#define FF_VDPAU_STATE_USED_FOR_REFERENCE 2
/**
* @brief This structure is used as a callback between the FFmpeg
* decoder (vd_) and presentation (vo_) module.
* This is used for defining a video frame containing surface,
* picture parameter, bitstream information etc which are passed
* between the FFmpeg decoder and its clients.
*/
struct vdpau_render_state {
VdpVideoSurface surface; ///< Used as rendered surface, never changed.
int state; ///< Holds FF_VDPAU_STATE_* values.
#if AV_HAVE_INCOMPATIBLE_LIBAV_ABI
/** picture parameter information for all supported codecs */
union AVVDPAUPictureInfo info;
#endif
/** Describe size/location of the compressed video data.
Set to 0 when freeing bitstream_buffers. */
int bitstream_buffers_allocated;
int bitstream_buffers_used;
/** The user is responsible for freeing this buffer using av_freep(). */
VdpBitstreamBuffer *bitstream_buffers;
#if !AV_HAVE_INCOMPATIBLE_LIBAV_ABI
/** picture parameter information for all supported codecs */
union AVVDPAUPictureInfo info;
#endif
};
#endif
/* @}*/
#endif /* AVCODEC_VDPAU_H */
@@ -0,0 +1,104 @@
/*
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_VERSION_H
#define AVCODEC_VERSION_H
/**
* @file
* @ingroup libavc
* Libavcodec version macros.
*/
#include "libavutil/avutil.h"
#define LIBAVCODEC_VERSION_MAJOR 55
#define LIBAVCODEC_VERSION_MINOR 39
#define LIBAVCODEC_VERSION_MICRO 101
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
LIBAVCODEC_VERSION_MINOR, \
LIBAVCODEC_VERSION_MICRO)
#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
LIBAVCODEC_VERSION_MINOR, \
LIBAVCODEC_VERSION_MICRO)
#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
/**
* FF_API_* defines may be placed below to indicate public API that will be
* dropped at a future version bump. The defines themselves are not part of
* the public API and may change, break or disappear at any time.
*/
#ifndef FF_API_REQUEST_CHANNELS
#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_ALLOC_CONTEXT
#define FF_API_ALLOC_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 55)
#endif
#ifndef FF_API_AVCODEC_OPEN
#define FF_API_AVCODEC_OPEN (LIBAVCODEC_VERSION_MAJOR < 55)
#endif
#ifndef FF_API_OLD_DECODE_AUDIO
#define FF_API_OLD_DECODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_OLD_TIMECODE
#define FF_API_OLD_TIMECODE (LIBAVCODEC_VERSION_MAJOR < 55)
#endif
#ifndef FF_API_OLD_ENCODE_AUDIO
#define FF_API_OLD_ENCODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_OLD_ENCODE_VIDEO
#define FF_API_OLD_ENCODE_VIDEO (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_CODEC_ID
#define FF_API_CODEC_ID (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_AVCODEC_RESAMPLE
#define FF_API_AVCODEC_RESAMPLE (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_DEINTERLACE
#define FF_API_DEINTERLACE (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_DESTRUCT_PACKET
#define FF_API_DESTRUCT_PACKET (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_GET_BUFFER
#define FF_API_GET_BUFFER (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_MISSING_SAMPLE
#define FF_API_MISSING_SAMPLE (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_LOWRES
#define FF_API_LOWRES (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_CAP_VDPAU
#define FF_API_CAP_VDPAU (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_BUFS_VDPAU
#define FF_API_BUFS_VDPAU (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_VOXWARE
#define FF_API_VOXWARE (LIBAVCODEC_VERSION_MAJOR < 56)
#endif
#endif /* AVCODEC_VERSION_H */
@@ -0,0 +1,168 @@
/*
* Copyright (C) 2003 Ivan Kalvachev
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_XVMC_H
#define AVCODEC_XVMC_H
/**
* @file
* @ingroup lavc_codec_hwaccel_xvmc
* Public libavcodec XvMC header.
*/
#include <X11/extensions/XvMC.h>
#include "avcodec.h"
/**
* @defgroup lavc_codec_hwaccel_xvmc XvMC
* @ingroup lavc_codec_hwaccel
*
* @{
*/
#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct
the number is 1337 speak for the letters IDCT MCo (motion compensation) */
struct xvmc_pix_fmt {
/** The field contains the special constant value AV_XVMC_ID.
It is used as a test that the application correctly uses the API,
and that there is no corruption caused by pixel routines.
- application - set during initialization
- libavcodec - unchanged
*/
int xvmc_id;
/** Pointer to the block array allocated by XvMCCreateBlocks().
The array has to be freed by XvMCDestroyBlocks().
Each group of 64 values represents one data block of differential
pixel information (in MoCo mode) or coefficients for IDCT.
- application - set the pointer during initialization
- libavcodec - fills coefficients/pixel data into the array
*/
short* data_blocks;
/** Pointer to the macroblock description array allocated by
XvMCCreateMacroBlocks() and freed by XvMCDestroyMacroBlocks().
- application - set the pointer during initialization
- libavcodec - fills description data into the array
*/
XvMCMacroBlock* mv_blocks;
/** Number of macroblock descriptions that can be stored in the mv_blocks
array.
- application - set during initialization
- libavcodec - unchanged
*/
int allocated_mv_blocks;
/** Number of blocks that can be stored at once in the data_blocks array.
- application - set during initialization
- libavcodec - unchanged
*/
int allocated_data_blocks;
/** Indicate that the hardware would interpret data_blocks as IDCT
coefficients and perform IDCT on them.
- application - set during initialization
- libavcodec - unchanged
*/
int idct;
/** In MoCo mode it indicates that intra macroblocks are assumed to be in
unsigned format; same as the XVMC_INTRA_UNSIGNED flag.
- application - set during initialization
- libavcodec - unchanged
*/
int unsigned_intra;
/** Pointer to the surface allocated by XvMCCreateSurface().
It has to be freed by XvMCDestroySurface() on application exit.
It identifies the frame and its state on the video hardware.
- application - set during initialization
- libavcodec - unchanged
*/
XvMCSurface* p_surface;
/** Set by the decoder before calling ff_draw_horiz_band(),
needed by the XvMCRenderSurface function. */
//@{
/** Pointer to the surface used as past reference
- application - unchanged
- libavcodec - set
*/
XvMCSurface* p_past_surface;
/** Pointer to the surface used as future reference
- application - unchanged
- libavcodec - set
*/
XvMCSurface* p_future_surface;
/** top/bottom field or frame
- application - unchanged
- libavcodec - set
*/
unsigned int picture_structure;
/** XVMC_SECOND_FIELD - 1st or 2nd field in the sequence
- application - unchanged
- libavcodec - set
*/
unsigned int flags;
//}@
/** Number of macroblock descriptions in the mv_blocks array
that have already been passed to the hardware.
- application - zeroes it on get_buffer().
A successful ff_draw_horiz_band() may increment it
with filled_mb_block_num or zero both.
- libavcodec - unchanged
*/
int start_mv_blocks_num;
/** Number of new macroblock descriptions in the mv_blocks array (after
start_mv_blocks_num) that are filled by libavcodec and have to be
passed to the hardware.
- application - zeroes it on get_buffer() or after successful
ff_draw_horiz_band().
- libavcodec - increment with one of each stored MB
*/
int filled_mv_blocks_num;
/** Number of the next free data block; one data block consists of
64 short values in the data_blocks array.
All blocks before this one have already been claimed by placing their
position into the corresponding block description structure field,
that are part of the mv_blocks array.
- application - zeroes it on get_buffer().
A successful ff_draw_horiz_band() may zero it together
with start_mb_blocks_num.
- libavcodec - each decoded macroblock increases it by the number
of coded blocks it contains.
*/
int next_free_data_block_num;
};
/**
* @}
*/
#endif /* AVCODEC_XVMC_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,481 @@
/*
* copyright (c) 2001 Fabrice Bellard
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVFORMAT_AVIO_H
#define AVFORMAT_AVIO_H
/**
* @file
* @ingroup lavf_io
* Buffered I/O operations
*/
#include <stdint.h>
#include "libavutil/common.h"
#include "libavutil/dict.h"
#include "libavutil/log.h"
#include "libavformat/version.h"
#define AVIO_SEEKABLE_NORMAL 0x0001 /**< Seeking works like for a local file */
/**
* Callback for checking whether to abort blocking functions.
* AVERROR_EXIT is returned in this case by the interrupted
* function. During blocking operations, callback is called with
* opaque as parameter. If the callback returns 1, the
* blocking operation will be aborted.
*
* No members can be added to this struct without a major bump, if
* new elements have been added after this struct in AVFormatContext
* or AVIOContext.
*/
typedef struct AVIOInterruptCB {
int (*callback)(void*);
void *opaque;
} AVIOInterruptCB;
/**
* Bytestream IO Context.
* New fields can be added to the end with minor version bumps.
* Removal, reordering and changes to existing fields require a major
* version bump.
* sizeof(AVIOContext) must not be used outside libav*.
*
* @note None of the function pointers in AVIOContext should be called
* directly, they should only be set by the client application
* when implementing custom I/O. Normally these are set to the
* function pointers specified in avio_alloc_context()
*/
typedef struct AVIOContext {
/**
* A class for private options.
*
* If this AVIOContext is created by avio_open2(), av_class is set and
* passes the options down to protocols.
*
* If this AVIOContext is manually allocated, then av_class may be set by
* the caller.
*
* warning -- this field can be NULL, be sure to not pass this AVIOContext
* to any av_opt_* functions in that case.
*/
const AVClass *av_class;
unsigned char *buffer; /**< Start of the buffer. */
int buffer_size; /**< Maximum buffer size */
unsigned char *buf_ptr; /**< Current position in the buffer */
unsigned char *buf_end; /**< End of the data, may be less than
buffer+buffer_size if the read function returned
less data than requested, e.g. for streams where
no more data has been received yet. */
void *opaque; /**< A private pointer, passed to the read/write/seek/...
functions. */
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size);
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size);
int64_t (*seek)(void *opaque, int64_t offset, int whence);
int64_t pos; /**< position in the file of the current buffer */
int must_flush; /**< true if the next seek should flush */
int eof_reached; /**< true if eof reached */
int write_flag; /**< true if open for writing */
int max_packet_size;
unsigned long checksum;
unsigned char *checksum_ptr;
unsigned long (*update_checksum)(unsigned long checksum, const uint8_t *buf, unsigned int size);
int error; /**< contains the error code or 0 if no error happened */
/**
* Pause or resume playback for network streaming protocols - e.g. MMS.
*/
int (*read_pause)(void *opaque, int pause);
/**
* Seek to a given timestamp in stream with the specified stream_index.
* Needed for some network streaming protocols which don't support seeking
* to byte position.
*/
int64_t (*read_seek)(void *opaque, int stream_index,
int64_t timestamp, int flags);
/**
* A combination of AVIO_SEEKABLE_ flags or 0 when the stream is not seekable.
*/
int seekable;
/**
* max filesize, used to limit allocations
* This field is internal to libavformat and access from outside is not allowed.
*/
int64_t maxsize;
/**
* avio_read and avio_write should if possible be satisfied directly
* instead of going through a buffer, and avio_seek will always
* call the underlying seek function directly.
*/
int direct;
/**
* Bytes read statistic
* This field is internal to libavformat and access from outside is not allowed.
*/
int64_t bytes_read;
/**
* seek statistic
* This field is internal to libavformat and access from outside is not allowed.
*/
int seek_count;
/**
* writeout statistic
* This field is internal to libavformat and access from outside is not allowed.
*/
int writeout_count;
} AVIOContext;
/* unbuffered I/O */
/**
* Return AVIO_FLAG_* access flags corresponding to the access permissions
* of the resource in url, or a negative value corresponding to an
* AVERROR code in case of failure. The returned access flags are
* masked by the value in flags.
*
* @note This function is intrinsically unsafe, in the sense that the
* checked resource may change its existence or permission status from
* one call to another. Thus you should not trust the returned value,
* unless you are sure that no other processes are accessing the
* checked resource.
*/
int avio_check(const char *url, int flags);
/**
* Allocate and initialize an AVIOContext for buffered I/O. It must be later
* freed with av_free().
*
* @param buffer Memory block for input/output operations via AVIOContext.
* The buffer must be allocated with av_malloc() and friends.
* @param buffer_size The buffer size is very important for performance.
* For protocols with fixed blocksize it should be set to this blocksize.
* For others a typical size is a cache page, e.g. 4kb.
* @param write_flag Set to 1 if the buffer should be writable, 0 otherwise.
* @param opaque An opaque pointer to user-specific data.
* @param read_packet A function for refilling the buffer, may be NULL.
* @param write_packet A function for writing the buffer contents, may be NULL.
* The function may not change the input buffers content.
* @param seek A function for seeking to specified byte position, may be NULL.
*
* @return Allocated AVIOContext or NULL on failure.
*/
AVIOContext *avio_alloc_context(
unsigned char *buffer,
int buffer_size,
int write_flag,
void *opaque,
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
int64_t (*seek)(void *opaque, int64_t offset, int whence));
void avio_w8(AVIOContext *s, int b);
void avio_write(AVIOContext *s, const unsigned char *buf, int size);
void avio_wl64(AVIOContext *s, uint64_t val);
void avio_wb64(AVIOContext *s, uint64_t val);
void avio_wl32(AVIOContext *s, unsigned int val);
void avio_wb32(AVIOContext *s, unsigned int val);
void avio_wl24(AVIOContext *s, unsigned int val);
void avio_wb24(AVIOContext *s, unsigned int val);
void avio_wl16(AVIOContext *s, unsigned int val);
void avio_wb16(AVIOContext *s, unsigned int val);
/**
* Write a NULL-terminated string.
* @return number of bytes written.
*/
int avio_put_str(AVIOContext *s, const char *str);
/**
* Convert an UTF-8 string to UTF-16LE and write it.
* @return number of bytes written.
*/
int avio_put_str16le(AVIOContext *s, const char *str);
/**
* Passing this as the "whence" parameter to a seek function causes it to
* return the filesize without seeking anywhere. Supporting this is optional.
* If it is not supported then the seek function will return <0.
*/
#define AVSEEK_SIZE 0x10000
/**
* Oring this flag as into the "whence" parameter to a seek function causes it to
* seek by any means (like reopening and linear reading) or other normally unreasonable
* means that can be extremely slow.
* This may be ignored by the seek code.
*/
#define AVSEEK_FORCE 0x20000
/**
* fseek() equivalent for AVIOContext.
* @return new position or AVERROR.
*/
int64_t avio_seek(AVIOContext *s, int64_t offset, int whence);
/**
* Skip given number of bytes forward
* @return new position or AVERROR.
*/
int64_t avio_skip(AVIOContext *s, int64_t offset);
/**
* ftell() equivalent for AVIOContext.
* @return position or AVERROR.
*/
static av_always_inline int64_t avio_tell(AVIOContext *s)
{
return avio_seek(s, 0, SEEK_CUR);
}
/**
* Get the filesize.
* @return filesize or AVERROR
*/
int64_t avio_size(AVIOContext *s);
/**
* feof() equivalent for AVIOContext.
* @return non zero if and only if end of file
*/
int url_feof(AVIOContext *s);
/** @warning currently size is limited */
int avio_printf(AVIOContext *s, const char *fmt, ...) av_printf_format(2, 3);
/**
* Force flushing of buffered data to the output s.
*
* Force the buffered data to be immediately written to the output,
* without to wait to fill the internal buffer.
*/
void avio_flush(AVIOContext *s);
/**
* Read size bytes from AVIOContext into buf.
* @return number of bytes read or AVERROR
*/
int avio_read(AVIOContext *s, unsigned char *buf, int size);
/**
* @name Functions for reading from AVIOContext
* @{
*
* @note return 0 if EOF, so you cannot use it if EOF handling is
* necessary
*/
int avio_r8 (AVIOContext *s);
unsigned int avio_rl16(AVIOContext *s);
unsigned int avio_rl24(AVIOContext *s);
unsigned int avio_rl32(AVIOContext *s);
uint64_t avio_rl64(AVIOContext *s);
unsigned int avio_rb16(AVIOContext *s);
unsigned int avio_rb24(AVIOContext *s);
unsigned int avio_rb32(AVIOContext *s);
uint64_t avio_rb64(AVIOContext *s);
/**
* @}
*/
/**
* Read a string from pb into buf. The reading will terminate when either
* a NULL character was encountered, maxlen bytes have been read, or nothing
* more can be read from pb. The result is guaranteed to be NULL-terminated, it
* will be truncated if buf is too small.
* Note that the string is not interpreted or validated in any way, it
* might get truncated in the middle of a sequence for multi-byte encodings.
*
* @return number of bytes read (is always <= maxlen).
* If reading ends on EOF or error, the return value will be one more than
* bytes actually read.
*/
int avio_get_str(AVIOContext *pb, int maxlen, char *buf, int buflen);
/**
* Read a UTF-16 string from pb and convert it to UTF-8.
* The reading will terminate when either a null or invalid character was
* encountered or maxlen bytes have been read.
* @return number of bytes read (is always <= maxlen)
*/
int avio_get_str16le(AVIOContext *pb, int maxlen, char *buf, int buflen);
int avio_get_str16be(AVIOContext *pb, int maxlen, char *buf, int buflen);
/**
* @name URL open modes
* The flags argument to avio_open must be one of the following
* constants, optionally ORed with other flags.
* @{
*/
#define AVIO_FLAG_READ 1 /**< read-only */
#define AVIO_FLAG_WRITE 2 /**< write-only */
#define AVIO_FLAG_READ_WRITE (AVIO_FLAG_READ|AVIO_FLAG_WRITE) /**< read-write pseudo flag */
/**
* @}
*/
/**
* Use non-blocking mode.
* If this flag is set, operations on the context will return
* AVERROR(EAGAIN) if they can not be performed immediately.
* If this flag is not set, operations on the context will never return
* AVERROR(EAGAIN).
* Note that this flag does not affect the opening/connecting of the
* context. Connecting a protocol will always block if necessary (e.g. on
* network protocols) but never hang (e.g. on busy devices).
* Warning: non-blocking protocols is work-in-progress; this flag may be
* silently ignored.
*/
#define AVIO_FLAG_NONBLOCK 8
/**
* Use direct mode.
* avio_read and avio_write should if possible be satisfied directly
* instead of going through a buffer, and avio_seek will always
* call the underlying seek function directly.
*/
#define AVIO_FLAG_DIRECT 0x8000
/**
* Create and initialize a AVIOContext for accessing the
* resource indicated by url.
* @note When the resource indicated by url has been opened in
* read+write mode, the AVIOContext can be used only for writing.
*
* @param s Used to return the pointer to the created AVIOContext.
* In case of failure the pointed to value is set to NULL.
* @param flags flags which control how the resource indicated by url
* is to be opened
* @return >= 0 in case of success, a negative value corresponding to an
* AVERROR code in case of failure
*/
int avio_open(AVIOContext **s, const char *url, int flags);
/**
* Create and initialize a AVIOContext for accessing the
* resource indicated by url.
* @note When the resource indicated by url has been opened in
* read+write mode, the AVIOContext can be used only for writing.
*
* @param s Used to return the pointer to the created AVIOContext.
* In case of failure the pointed to value is set to NULL.
* @param flags flags which control how the resource indicated by url
* is to be opened
* @param int_cb an interrupt callback to be used at the protocols level
* @param options A dictionary filled with protocol-private options. On return
* this parameter will be destroyed and replaced with a dict containing options
* that were not found. May be NULL.
* @return >= 0 in case of success, a negative value corresponding to an
* AVERROR code in case of failure
*/
int avio_open2(AVIOContext **s, const char *url, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options);
/**
* Close the resource accessed by the AVIOContext s and free it.
* This function can only be used if s was opened by avio_open().
*
* The internal buffer is automatically flushed before closing the
* resource.
*
* @return 0 on success, an AVERROR < 0 on error.
* @see avio_closep
*/
int avio_close(AVIOContext *s);
/**
* Close the resource accessed by the AVIOContext *s, free it
* and set the pointer pointing to it to NULL.
* This function can only be used if s was opened by avio_open().
*
* The internal buffer is automatically flushed before closing the
* resource.
*
* @return 0 on success, an AVERROR < 0 on error.
* @see avio_close
*/
int avio_closep(AVIOContext **s);
/**
* Open a write only memory stream.
*
* @param s new IO context
* @return zero if no error.
*/
int avio_open_dyn_buf(AVIOContext **s);
/**
* Return the written size and a pointer to the buffer. The buffer
* must be freed with av_free().
* Padding of FF_INPUT_BUFFER_PADDING_SIZE is added to the buffer.
*
* @param s IO context
* @param pbuffer pointer to a byte buffer
* @return the length of the byte buffer
*/
int avio_close_dyn_buf(AVIOContext *s, uint8_t **pbuffer);
/**
* Iterate through names of available protocols.
*
* @param opaque A private pointer representing current protocol.
* It must be a pointer to NULL on first iteration and will
* be updated by successive calls to avio_enum_protocols.
* @param output If set to 1, iterate over output protocols,
* otherwise over input protocols.
*
* @return A static string containing the name of current protocol or NULL
*/
const char *avio_enum_protocols(void **opaque, int output);
/**
* Pause and resume playing - only meaningful if using a network streaming
* protocol (e.g. MMS).
* @param pause 1 for pause, 0 for resume
*/
int avio_pause(AVIOContext *h, int pause);
/**
* Seek to a given timestamp relative to some component stream.
* Only meaningful if using a network streaming protocol (e.g. MMS.).
* @param stream_index The stream index that the timestamp is relative to.
* If stream_index is (-1) the timestamp should be in AV_TIME_BASE
* units from the beginning of the presentation.
* If a stream_index >= 0 is used and the protocol does not support
* seeking based on component streams, the call will fail.
* @param timestamp timestamp in AVStream.time_base units
* or if there is no stream specified then in AV_TIME_BASE units.
* @param flags Optional combination of AVSEEK_FLAG_BACKWARD, AVSEEK_FLAG_BYTE
* and AVSEEK_FLAG_ANY. The protocol may silently ignore
* AVSEEK_FLAG_BACKWARD and AVSEEK_FLAG_ANY, but AVSEEK_FLAG_BYTE will
* fail if used and not supported.
* @return >= 0 on success
* @see AVInputFormat::read_seek
*/
int64_t avio_seek_time(AVIOContext *h, int stream_index,
int64_t timestamp, int flags);
#endif /* AVFORMAT_AVIO_H */
@@ -0,0 +1,76 @@
/*
* Version macros.
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVFORMAT_VERSION_H
#define AVFORMAT_VERSION_H
/**
* @file
* @ingroup libavf
* Libavformat version macros
*/
#include "libavutil/avutil.h"
#define LIBAVFORMAT_VERSION_MAJOR 55
#define LIBAVFORMAT_VERSION_MINOR 19
#define LIBAVFORMAT_VERSION_MICRO 104
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
LIBAVFORMAT_VERSION_MINOR, \
LIBAVFORMAT_VERSION_MICRO)
#define LIBAVFORMAT_VERSION AV_VERSION(LIBAVFORMAT_VERSION_MAJOR, \
LIBAVFORMAT_VERSION_MINOR, \
LIBAVFORMAT_VERSION_MICRO)
#define LIBAVFORMAT_BUILD LIBAVFORMAT_VERSION_INT
#define LIBAVFORMAT_IDENT "Lavf" AV_STRINGIFY(LIBAVFORMAT_VERSION)
/**
* FF_API_* defines may be placed below to indicate public API that will be
* dropped at a future version bump. The defines themselves are not part of
* the public API and may change, break or disappear at any time.
*/
#ifndef FF_API_ALLOC_OUTPUT_CONTEXT
#define FF_API_ALLOC_OUTPUT_CONTEXT (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_FORMAT_PARAMETERS
#define FF_API_FORMAT_PARAMETERS (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_NEW_STREAM
#define FF_API_NEW_STREAM (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_SET_PTS_INFO
#define FF_API_SET_PTS_INFO (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_CLOSE_INPUT_FILE
#define FF_API_CLOSE_INPUT_FILE (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_READ_PACKET
#define FF_API_READ_PACKET (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_ASS_SSA
#define FF_API_ASS_SSA (LIBAVFORMAT_VERSION_MAJOR < 56)
#endif
#ifndef FF_API_R_FRAME_RATE
#define FF_API_R_FRAME_RATE 1
#endif
#endif /* AVFORMAT_VERSION_H */

Some files were not shown because too many files have changed in this diff Show More