Compare commits

..

94 Commits

Author SHA1 Message Date
Cameron Gutman 34f72544d8 Increment version 2014-11-25 14:56:40 -08:00
Cameron Gutman d839ea9781 Increase deadzone on triggers to Xinput defaults and add special handling of the Nexus Player Controller and Nexus Remote 2014-11-25 14:54:36 -08:00
Cameron Gutman 2b7f13fdbb Increase max frame time to improve accuracy of latency counter 2014-11-25 13:34:00 -08:00
Cameron Gutman 7557a3a4ae Don't capture the back button on remotes 2014-11-25 11:16:47 -08:00
Cameron Gutman fcecba484f Fix a crash caught by Monkey 2014-11-25 02:05:24 -08:00
Cameron Gutman fa85a0a0bd Improve CPU decoder frame latency when rendering speed is less than decoding speed 2014-11-25 02:04:51 -08:00
Cameron Gutman dc64bfeba2 Slightly reduce max packet size in an attempt to cut packet losses 2014-11-25 01:05:55 -08:00
Cameron Gutman 871b73c48d Fix PC duplication issue when multiple machines report the same remote IP address 2014-11-24 20:10:02 -08:00
Cameron Gutman 5dcff91d27 Only grab Fire TV remotes if a gamepad isn't attached 2014-11-24 18:43:08 -08:00
Cameron Gutman 0041fc1dab Fix broken de-duplication of computers 2014-11-24 18:25:58 -08:00
Cameron Gutman 314242ab08 Update to Ion with fixes for SSLContext and self-signed certificates 2014-11-24 18:10:23 -08:00
Cameron Gutman 09e8ddfd74 Use the bitstream restrictions fixup on Broadcom VideoCore IV devices 2014-11-24 18:03:47 -08:00
Cameron Gutman 444c4602c1 Update libraries. Seems to improve image caching behavior with Ion. 2014-11-23 23:39:20 -08:00
Cameron Gutman 5b6eac7140 Update build.gradle for re-release 2014-11-23 02:07:03 -08:00
Cameron Gutman 7cdd184197 Fix null pointer exception on ATV emulator 2014-11-23 02:06:49 -08:00
Cameron Gutman be153b84cb Update build for final 3.0 release 2014-11-22 23:15:11 -08:00
Cameron Gutman 06c53e2251 Update decoder errata 2014-11-22 22:08:29 -08:00
Cameron Gutman 695519bdf5 Reduce Nexus Player video latency by 10x 2014-11-22 22:05:59 -08:00
Cameron Gutman bf7d033ab2 Don't use adaptive playback at all to avoid extra added latency on some decoders 2014-11-22 20:35:31 -08:00
Cameron Gutman df67795c4a Use back as start on Android TV 2014-11-22 19:33:26 -08:00
Cameron Gutman 72c1696f43 Fix missing PCs in PC list after my NPE fix 2014-11-21 22:56:56 -08:00
Cameron Gutman 8eca3683c9 Add method for getting video decoder name 2014-11-21 11:08:35 -08:00
Cameron Gutman 80c17b4913 Update common 2014-11-20 19:22:28 -08:00
Cameron Gutman e5050f10bb Fix a potential null pointer exception 2014-11-20 19:22:20 -08:00
Cameron Gutman e912e4de57 Don't do deadzone scaling because the PC should be handling that. Return to non-scaled controller packets. Disable the deadzone option in preferences. 2014-11-20 00:00:48 -08:00
Cameron Gutman 8dee1f0d80 Add a trigger deadzone 2014-11-19 23:59:42 -08:00
Cameron Gutman 53594ada66 Disable the Android TV controller hack for now 2014-11-19 23:27:10 -08:00
Cameron Gutman 848ed1ad72 Scale touch inputs based on the ratio of the stream size to the screen size 2014-11-19 23:26:50 -08:00
Cameron Gutman 307e807c8f Replay motion event history during input processing 2014-11-19 23:08:34 -08:00
Cameron Gutman 6a27780d56 Remove hat flat values 2014-11-19 22:57:17 -08:00
Cameron Gutman 57f98dbb4a Add missing import 2014-11-19 22:07:57 -08:00
Cameron Gutman 5af7d83ec1 Fix RTL Lint warnings by using start/end 2014-11-19 22:06:22 -08:00
Cameron Gutman 4a6f77f43a Remove an unused string 2014-11-19 22:05:51 -08:00
Cameron Gutman c96f9fb635 Prevent deadzone and bitrate from dropping below 1 2014-11-19 20:11:13 -08:00
Cameron Gutman e3a477a243 Don't send a bunch of duplicate controller packets if a button is being held down 2014-11-19 19:05:59 -08:00
Cameron Gutman 9fcd641143 Make the back button function as the start button on Android TV controllers (needs testing) 2014-11-19 18:40:22 -08:00
Cameron Gutman 6d1cbc5a64 Add a hack for the Tablet Remote app to fix the B button 2014-11-19 18:39:15 -08:00
Cameron Gutman ec71060d98 Fix broken keyboards and gamepads when an input device wasn't provided (such as a virtual gamepad or IME) 2014-11-19 18:37:47 -08:00
Cameron Gutman 03f706fb85 Update common 2014-11-19 10:43:09 -08:00
Cameron Gutman 7ad87bd3ee Small fix to the frame timing code 2014-11-19 10:43:00 -08:00
Cameron Gutman 4e088f6183 Fix minor grammar error 2014-11-18 19:09:23 -08:00
Cameron Gutman 1b16ea6f53 Merge pull request #31 from Ansa89/NewUI-italian-translation
NewUI: Add italian translation
2014-11-17 19:51:38 -08:00
Ansa89 f262503bc8 Italian translation: update 2014-11-17 09:50:17 +01:00
Cameron Gutman b2ba216cd1 Poll every 3 seconds instead of every 5 seconds 2014-11-16 18:14:50 -08:00
Cameron Gutman 94ba7f8e45 Fix a bunch of bugs in the new (and old) computer manager service 2014-11-16 18:09:31 -08:00
Cameron Gutman a267cf59c7 Increment version 2014-11-16 17:20:27 -08:00
Cameron Gutman 79e8bef289 Update common 2014-11-16 17:20:11 -08:00
Cameron Gutman 99e3b5f33b Rewrite a large portion of the computer manager service to fix some thread leaks and improve performance 2014-11-16 17:20:04 -08:00
Cameron Gutman afbe64f3ff Remove an unused import 2014-11-16 17:19:07 -08:00
Cameron Gutman 43b1a73ae0 Use a transparent background for the streaming activity to avoid overdraw 2014-11-16 17:18:53 -08:00
Cameron Gutman d08eeb8a2d Don't display a toast after pairing has completed 2014-11-16 16:37:41 -08:00
Cameron Gutman 7c39e5c974 Fix a race condition 2014-11-16 14:57:54 -08:00
Cameron Gutman cd49334199 If we've previously been able to reach a machine via a local or remote IP, always try that one first when polling on subsequent tries 2014-11-16 14:35:36 -08:00
Cameron Gutman dd59f0bc6d Fix app grid UI issues 2014-11-16 14:27:20 -08:00
Cameron Gutman cf2d83a1ea Fix comment typo 2014-11-16 14:23:58 -08:00
Cameron Gutman d5b6130936 Use 40% larger packets (1450 bytes) on local networks 2014-11-16 12:09:32 -08:00
Cameron Gutman 4ae29b0075 Improve performance of the CPU decoder and add some details about changing decoders 2014-11-16 11:52:08 -08:00
Ansa89 34e35cd493 Add italian translation 2014-11-14 11:19:03 +01:00
Cameron Gutman a17af070c5 Condense some text to better fit the UI 2014-11-13 23:30:42 -08:00
Cameron Gutman fbe0a26800 Select the PC grid when the down button is pressed when focused on one of the buttons 2014-11-13 23:28:23 -08:00
Cameron Gutman 25ad99df94 Update common 2014-11-13 23:22:37 -08:00
Cameron Gutman 6338e7b8eb Add deadzone preference 2014-11-13 23:22:13 -08:00
Cameron Gutman 1b9846d519 Close the app list instead of displaying an error if the app view is resumed and fails to update 2014-11-13 22:31:19 -08:00
Cameron Gutman a4ece13a1d Fix refreshing apps text 2014-11-13 22:30:45 -08:00
Cameron Gutman 066b8430a0 Update common with fix for 404 error message 2014-11-13 21:56:28 -08:00
Cameron Gutman 2b54a91f3d Replace ... with elipsis character 2014-11-13 21:50:12 -08:00
Cameron Gutman 2d01633372 Fix small error in strings.xml 2014-11-13 21:49:59 -08:00
Cameron Gutman 5dc01069fc Update PC view to avoid scrunched up text when looking for a PC on phones in portrait orientation 2014-11-13 21:49:44 -08:00
Cameron Gutman d450008833 Don't use deprecated constants 2014-11-13 21:49:12 -08:00
Cameron Gutman a37fff6eb5 Fix a bunch of Lint errors 2014-11-13 21:37:11 -08:00
Cameron Gutman 6604675bf9 Lint: Remove unused imports 2014-11-13 21:30:32 -08:00
Cameron Gutman 1965cc2347 Merge branch 'NewUI-prepare-for-translation' into NewUI
Conflicts:
	app/src/main/java/com/limelight/PcView.java
2014-11-13 21:30:11 -08:00
Cameron Gutman 312ca27906 Open the app list after successfully pairing 2014-11-13 21:22:45 -08:00
Cameron Gutman 0bceadbd9a New common with disabled FEC 2014-11-13 21:16:32 -08:00
Cameron Gutman dfc3daabcd Use a 2 frame audio buffer if possible to reduce audio latency 2014-11-13 21:16:14 -08:00
Ansa89 b9ba9adc1f Forgot about these 2014-11-12 12:52:18 +01:00
Ansa89 f112d45e1a Some cleanup 2014-11-12 09:58:26 +01:00
Ansa89 88f139873c Resolve merge conflicts 2014-11-12 09:41:12 +01:00
Ansa89 d317c5bf03 Try to make limelight more translatable 2014-11-11 16:30:20 +01:00
Cameron Gutman 9d72314b9c Update common again 2014-11-11 01:14:32 -08:00
Cameron Gutman 2cc7243573 Update beta version 2014-11-10 22:06:02 -08:00
Cameron Gutman 269d9a6bc6 Update to support GFE 2.1.4 2014-11-10 22:01:19 -08:00
Cameron Gutman 244130fc1b Add visual indication when no PCs have been found yet 2014-11-08 13:56:16 -08:00
Cameron Gutman a67791b8aa Display the delete PC option for local PCs too, even though it may not always work 2014-11-08 13:20:14 -08:00
Cameron Gutman 21e46a5c3b Display machines as they are being refreshed 2014-11-08 13:14:35 -08:00
Cameron Gutman 2df2f850d5 Remove dead code 2014-11-08 13:13:39 -08:00
Cameron Gutman 406d26ec1c Add visual feeback for offline machines and running games 2014-11-08 12:51:07 -08:00
Cameron Gutman 68c1aaf433 Add new app view UI 2014-11-08 01:07:21 -08:00
Cameron Gutman 9ef577dbdd Update UI for add PC 2014-11-07 23:09:45 -08:00
Cameron Gutman 982ecbc015 Improve the look of the buttons and PC view UI 2014-11-07 22:28:07 -08:00
Cameron Gutman 7e44b5abd5 Remove margins from landscape pc view 2014-11-07 01:20:55 -08:00
Cameron Gutman 6dbb1a0c1f Fix UI performance issues 2014-11-07 01:18:14 -08:00
Cameron Gutman 94b1c04fa6 GridView WIP 2014-11-07 00:27:58 -08:00
Cameron Gutman 9758276f1c Use the normal margins for AddComputerManually 2014-11-06 22:30:10 -08:00
49 changed files with 1730 additions and 734 deletions
+7 -1
View File
@@ -102,11 +102,17 @@
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-r7" 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="okhttp-2.1.0" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="okio-1.0.1" level="project" />
<orderEntry type="library" exported="" name="androidasync-e1dfb4" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="ion-2f46fa" level="project" />
</component>
</module>
+16 -6
View File
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.0.2"
buildToolsVersion "21.1.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 21
versionName "2.9"
versionCode = 38
versionName "3.0.1"
versionCode = 47
}
productFlavors {
@@ -62,9 +62,19 @@ android {
}
dependencies {
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: 'org.jcodec', name: 'jcodec', version: '+'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
compile group: 'com.google.android', name: 'support-v4', version:'+'
// FIXME: Pending resolution of issue #346 using custom build
//compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
compile group: 'com.google.code.gson', name: 'gson', version:'+'
compile files('libs/androidasync-e1dfb4.jar')
compile files('libs/ion-2f46fa.jar')
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
compile group: 'com.squareup.okio', name:'okio', version:'+'
compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar')
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -64,6 +64,7 @@
<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"
+98 -98
View File
@@ -9,10 +9,10 @@ import java.util.List;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
@@ -27,18 +27,17 @@ import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.GridView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity {
private ListView appList;
private ArrayAdapter<AppObject> appListAdapter;
private AppGridAdapter appGridAdapter;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private boolean firstLoad = true;
private final static int RESUME_ID = 1;
private final static int QUIT_ID = 2;
@@ -60,10 +59,11 @@ public class AppView extends Activity {
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
finish();
return;
}
String labelText = "App List for "+getIntent().getStringExtra(NAME_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);
@@ -71,34 +71,39 @@ public class AppView extends Activity {
try {
ipAddress = InetAddress.getByAddress(address);
} catch (UnknownHostException e) {
return;
e.printStackTrace();
finish();
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);
GridView appGrid = (GridView) findViewById(R.id.appGridView);
try {
appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId);
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
appGrid.setAdapter(appGridAdapter);
appGrid.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.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(appGrid);
}
@Override
@@ -112,14 +117,18 @@ public class AppView extends Activity {
@Override
protected void onResume() {
super.onResume();
updateAppList();
// Display the error message if it was the
// first load, but just kill the activity
// on subsequent errors
updateAppList(firstLoad);
firstLoad = false;
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = appListAdapter.getItem(i);
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
@@ -133,11 +142,11 @@ public class AppView extends Activity {
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = appListAdapter.getItem(info.position);
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
@@ -145,12 +154,12 @@ public class AppView extends Activity {
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
menu.add(Menu.NONE, 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, RESUME_ID, 1, "Quit Current Game and Start");
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
}
@@ -162,41 +171,28 @@ public class AppView extends Activity {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
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);
AppObject app = (AppObject) appGridAdapter.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 static String generateString(NvApp app) {
StringBuilder str = new StringBuilder();
str.append(app.getAppName());
if (app.getIsRunning()) {
str.append(" - Running");
}
return str.toString();
}
private void addListPlaceholder() {
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
}
private void updateAppList() {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
private void updateAppList(final boolean displayError) {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
new Thread() {
@Override
public void run() {
@@ -208,17 +204,12 @@ public class AppView extends Activity {
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();
appGridAdapter.clear();
for (NvApp app : appList) {
appGridAdapter.addApp(new AppObject(app));
}
appGridAdapter.notifyDataSetChanged();
}
});
@@ -230,8 +221,20 @@ public class AppView extends Activity {
} finally {
spinner.dismiss();
}
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
if (displayError) {
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
getResources().getString(R.string.applist_refresh_error_msg), true);
}
else {
// Just finish the activity immediately
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
}
});
}
}
}.start();
}
@@ -246,7 +249,7 @@ public class AppView extends Activity {
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
@@ -255,17 +258,16 @@ public class AppView extends Activity {
try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = "Successfully quit "+app.getAppName();
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
}
else {
message = "Failed to quit "+app.getAppName();
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
}
updateAppList();
updateAppList(true);
} catch (UnknownHostException e) {
message = "Failed to resolve host";
message = getResources().getString(R.string.error_unknown_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.";
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
}
@@ -282,17 +284,15 @@ public class AppView extends Activity {
}
public class AppObject {
public String text;
public NvApp app;
public AppObject(String text, NvApp app) {
this.text = text;
public AppObject(NvApp app) {
this.app = app;
}
@Override
public String toString() {
return text;
return app.getAppName();
}
}
}
}
+48 -30
View File
@@ -1,6 +1,7 @@
package com.limelight;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
@@ -113,12 +114,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Inflate the content
setContentView(R.layout.activity_game);
// Start the spinner
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.conn_establishing_msg), true);
// Read the stream preferences
prefConfig = PreferenceConfiguration.readPreferences(this);
prefConfig = PreferenceConfiguration.readPreferences(this);
switch (prefConfig.decoder) {
case PreferenceConfiguration.FORCE_SOFTWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
@@ -143,17 +145,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
sv.setOnTouchListener(this);
// Warn the user if they're on a metered connection
checkDataConnection();
// Make sure Wi-Fi is fully powered up
checkDataConnection();
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
@@ -167,12 +170,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292)
.build();
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn);
controllerHandler = new ControllerHandler(conn, prefConfig.deadzonePercentage);
SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
@@ -182,7 +186,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i);
touchContextMap[i] = new TouchContext(conn, i,
((double)prefConfig.width / (double)screenSize.x),
((double)prefConfig.height / (double)screenSize.y));
}
if (LimelightBuildProps.ROOT_BUILD) {
@@ -214,7 +220,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (mgr.isActiveNetworkMetered()) {
displayTransientMessage("Warning: Your active network connection is metered!");
displayTransientMessage(getResources().getString(R.string.conn_metered));
}
}
@@ -262,13 +268,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = "Average client-side frame latency: "+averageEndToEndLat+" ms";
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
}
}
else if (averageDecoderLat > 0) {
message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
}
if (message != null) {
@@ -383,18 +389,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyDown(keyCode, event);
}
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyDown(keyCode, event);
}
// Try the controller handler first
boolean handled = controllerHandler.handleButtonDown(keyCode, event);
boolean handled = controllerHandler.handleButtonDown(event);
if (!handled) {
// Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode());
@@ -426,18 +427,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyUp(keyCode, event);
}
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyUp(keyCode, event);
}
// Try the controller handler first
boolean handled = controllerHandler.handleButtonUp(keyCode, event);
boolean handled = controllerHandler.handleButtonUp(event);
if (!handled) {
// Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode());
@@ -516,6 +512,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE is special because it always has actionIndex == 0
// We'll call the move handlers for all indexes manually
// First process the historical events
for (int i = 0; i < event.getHistorySize(); i++) {
for (TouchContext aTouchContextMap : touchContextMap) {
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
aTouchContextMap.touchMoveEvent(
(int)event.getHistoricalX(aTouchContextMap.getActionIndex(), i),
(int)event.getHistoricalY(aTouchContextMap.getActionIndex(), i));
}
}
}
// Now process the current values
for (TouchContext aTouchContextMap : touchContextMap) {
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
@@ -567,9 +577,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
updateMousePosition((int)event.getX(), (int)event.getY());
// First process the history
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
lastButtonState = event.getButtonState();
// Now process the current values
updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState();
}
else
{
@@ -634,7 +650,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void stageStarting(Stage stage) {
if (spinner != null) {
spinner.setMessage("Starting "+stage.getName());
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName());
}
}
@@ -665,7 +681,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (!displayedFailureDialog) {
displayedFailureDialog = true;
stopConnection();
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
}
}
@@ -676,7 +693,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
e.printStackTrace();
stopConnection();
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
}
+123 -174
View File
@@ -9,6 +9,7 @@ import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
@@ -36,16 +37,15 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.GridView;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity {
private Button settingsButton, addComputerButton;
private ListView pcList;
private ArrayAdapter<ComputerObject> pcListAdapter;
private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() {
@@ -99,56 +99,53 @@ public class PcView extends Activity {
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view
settingsButton = (Button)findViewById(R.id.settingsButton);
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
pcList = (ListView)findViewById(R.id.pcListView);
pcList.setAdapter(pcListAdapter);
pcList.setItemsCanFocus(true);
pcList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = pcListAdapter.getItem(pos);
if (computer.details == null) {
// Placeholder item; no context menu for it
return;
}
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
}
else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
}
else {
doAppList(computer.details);
}
}
});
registerForContextMenu(pcList);
GridView pcGrid = (GridView) findViewById(R.id.pcGridView);
pcGrid.setAdapter(pcGridAdapter);
pcGrid.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
}
}
});
registerForContextMenu(pcGrid);
settingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(PcView.this, StreamSettings.class));
}
});
@Override
public void onClick(View v) {
startActivity(new Intent(PcView.this, StreamSettings.class));
}
});
addComputerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(PcView.this, AddComputerManually.class);
startActivity(i);
}
});
@Override
public void onClick(View v) {
Intent i = new Intent(PcView.this, AddComputerManually.class);
startActivity(i);
}
});
if (pcListAdapter.isEmpty()) {
addListPlaceholder();
}
else {
pcListAdapter.notifyDataSetChanged();
}
}
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE);
}
else {
noPcFoundLayout.setVisibility(View.INVISIBLE);
}
pcGridAdapter.notifyDataSetChanged();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -157,9 +154,8 @@ public class PcView extends Activity {
// Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
pcListAdapter.setNotifyOnChange(false);
pcGridAdapter = new PcGridAdapter(this);
initializeViews();
}
@@ -178,7 +174,7 @@ public class PcView extends Activity {
PcView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
updateListView(details);
updateComputer(details);
}
});
}
@@ -237,33 +233,32 @@ public class PcView extends Activity {
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
stopComputerUpdates(false);
// Call superclass
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = pcListAdapter.getItem(info.position);
if (computer == null || computer.details == null) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer == null || computer.details == null ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates();
return;
}
// Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else if (computer.details.pairState != PairState.PAIRED) {
menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
}
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else {
menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
}
}
@@ -274,26 +269,25 @@ public class PcView extends Activity {
private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
if (computer.runningGameId != 0) {
Toast.makeText(PcView.this, "Computer is currently in a game. " +
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
boolean success = false;
try {
// Stop updates and wait while pairing
stopComputerUpdates(true);
@@ -311,23 +305,28 @@ public class PcView extends Activity {
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
message = "Already paired";
// Don't display any toast, but open the app list
message = null;
success = true;
}
else {
final String pinStr = PairingManager.generatePinString();
// Spin the dialog off in a thread because it blocks
Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) {
message = "Incorrect PIN";
message = getResources().getString(R.string.pair_incorrect_pin);
}
else if (pairState == PairingManager.PairState.FAILED) {
message = "Pairing failed";
message = getResources().getString(R.string.pair_fail);
}
else if (pairState == PairingManager.PairState.PAIRED) {
message = "Paired successfully";
// Just navigate to the app view without displaying a toast
message = null;
success = true;
}
else {
// Should be no other values
@@ -335,21 +334,29 @@ public class PcView extends Activity {
}
}
} catch (UnknownHostException e) {
message = "Failed to resolve host";
message = getResources().getString(R.string.error_unknown_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.";
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
e.printStackTrace();
message = e.getMessage();
}
Dialog.closeDialogs();
final String toastMessage = message;
final boolean toastSuccess = success;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
if (toastMessage != null) {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
if (toastSuccess) {
// Open the app list after a successful pairing attemp
doAppList(computer);
}
}
});
@@ -361,26 +368,25 @@ public class PcView extends Activity {
private void doWakeOnLan(final ComputerDetails computer) {
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
return;
}
if (computer.macAddress == null) {
Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
String message;
try {
WakeOnLanSender.sendWolPacket(computer);
message = "It may take a few seconds for your PC to wake up. " +
"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
message = getResources().getString(R.string.wol_waking_msg);
} catch (IOException e) {
message = "Failed to send Wake-On-LAN packets";
message = getResources().getString(R.string.wol_fail);
}
final String toastMessage = message;
@@ -396,16 +402,15 @@ public class PcView extends Activity {
private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
@@ -427,20 +432,19 @@ public class PcView extends Activity {
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair();
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
message = "Unpaired successfully";
message = getResources().getString(R.string.unpair_success);
}
else {
message = "Failed to unpair";
message = getResources().getString(R.string.unpair_fail);
}
}
else {
message = "Device was not paired";
message = getResources().getString(R.string.unpair_error);
}
} catch (UnknownHostException e) {
message = "Failed to resolve host";
message = getResources().getString(R.string.error_unknown_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.";
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
}
@@ -458,12 +462,11 @@ public class PcView extends Activity {
private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
@@ -485,7 +488,7 @@ public class PcView extends Activity {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
ComputerObject computer = pcListAdapter.getItem(info.position);
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId())
{
case PAIR_ID:
@@ -502,12 +505,11 @@ public class PcView extends Activity {
case DELETE_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
managerBinder.removeComputer(computer.details.name);
removeListView(computer.details);
removeComputer(computer.details);
return true;
case APP_LIST_ID:
@@ -518,71 +520,23 @@ public class PcView extends Activity {
return super.onContextItemSelected(item);
}
}
private static String generateString(ComputerDetails details) {
StringBuilder str = new StringBuilder();
str.append(details.name).append(" - ");
if (details.state == ComputerDetails.State.ONLINE) {
str.append("Online ");
if (details.reachability == ComputerDetails.Reachability.LOCAL) {
str.append("(Local) - ");
}
else {
str.append("(Remote) - ");
}
if (details.pairState == PairState.PAIRED) {
if (details.runningGameId == 0) {
str.append("Available");
}
else {
str.append("In Game");
}
}
else {
str.append("Not Paired");
}
}
else {
str.append("Offline");
}
return str.toString();
}
private void addListPlaceholder() {
pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
"If your PC doesn't show up in about 15 seconds, " +
"make sure your computer is running GFE or add your PC manually using the button above.", null));
}
private void removeListView(ComputerDetails details) {
for (int i = 0; i < pcListAdapter.getCount(); i++) {
ComputerObject computer = pcListAdapter.getItem(i);
private void removeComputer(ComputerDetails details) {
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
if (details.equals(computer.details)) {
pcListAdapter.remove(computer);
pcGridAdapter.removeComputer(computer);
break;
}
}
if (pcListAdapter.getCount() == 0) {
// Add the placeholder if we're down to 0 computers
addListPlaceholder();
}
}
private void updateListView(ComputerDetails details) {
String computerString = generateString(details);
private void updateComputer(ComputerDetails details) {
ComputerObject existingEntry = null;
boolean placeholderPresent = false;
for (int i = 0; i < pcListAdapter.getCount(); i++) {
ComputerObject computer = pcListAdapter.getItem(i);
// If there's a placeholder, there's nothing else
if (computer.details == null) {
placeholderPresent = true;
break;
}
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer
if (details.equals(computer.details)) {
@@ -593,35 +547,30 @@ public class PcView extends Activity {
if (existingEntry != null) {
// Replace the information in the existing entry
existingEntry.text = computerString;
existingEntry.details = details;
}
else {
// If the placeholder is the only object, remove it
if (placeholderPresent) {
pcListAdapter.remove(pcListAdapter.getItem(0));
}
// Add a new entry
pcListAdapter.add(new ComputerObject(computerString, details));
pcGridAdapter.addComputer(new ComputerObject(details));
// Remove the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.INVISIBLE);
}
// Notify the view that the data has changed
pcListAdapter.notifyDataSetChanged();
// Notify the view that the data has changed
pcGridAdapter.notifyDataSetChanged();
}
public class ComputerObject {
public String text;
public ComputerDetails details;
public ComputerObject(String text, ComputerDetails details) {
this.text = text;
public ComputerObject(ComputerDetails details) {
this.details = details;
}
@Override
public String toString() {
return text;
return details.name;
}
}
}
@@ -31,23 +31,50 @@ public class AndroidAudioRenderer implements AudioRenderer {
return false;
}
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);
// 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 = FRAME_SIZE * 2;
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) {}
// Now try the larger buffer size
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);
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);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
return true;
}
@@ -7,6 +7,7 @@ import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.utils.Vector2d;
@@ -44,12 +45,47 @@ public class ControllerHandler {
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
private NvConnection conn;
private double stickDeadzone;
private final ControllerMapping defaultMapping = new ControllerMapping();
private boolean hasGameController;
public ControllerHandler(NvConnection conn) {
public ControllerHandler(NvConnection conn, int deadzonePercentage) {
this.conn = conn;
// We want limelight-common to scale the axis values to match Xinput values
ControllerPacket.enableAxisScaling = true;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work.
deadzonePercentage = 10;
int[] ids = InputDevice.getDeviceIds();
for (int i = 0; i < ids.length; i++) {
InputDevice dev = InputDevice.getDevice(ids[i]);
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) != null) {
// This is a gamepad
hasGameController = true;
}
}
}
// 1% is the lowest possible deadzone we support
if (deadzonePercentage <= 0) {
deadzonePercentage = 1;
}
this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default mapping for events with no device
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -67,9 +103,18 @@ public class ControllerHandler {
private ControllerMapping createMappingForDevice(InputDevice dev) {
ControllerMapping mapping = new ControllerMapping();
mapping.leftStickXAxis = MotionEvent.AXIS_X;
String devName = dev.getName();
LimeLog.info("Creating controller mapping for device: "+devName);
mapping.leftStickXAxis = MotionEvent.AXIS_X;
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
// This is a gamepad
hasGameController = true;
mapping.hasJoystickAxes = true;
}
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
@@ -91,8 +136,7 @@ public class ControllerHandler {
{
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null) {
String devName = dev.getName();
if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
@@ -142,70 +186,68 @@ public class ControllerHandler {
if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
mapping.hatXDeadzone = hatXRange.getFlat();
mapping.hatYDeadzone = hatYRange.getFlat();
}
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
if (lsXRange != null && lsYRange != null) {
// The flat values should never be negative but we'll deal with it if they are
mapping.leftStickDeadzoneRadius = Math.max(Math.abs(lsXRange.getFlat()),
Math.abs(lsYRange.getFlat()));
// Some devices (certain OUYAs at least) report a deadzone that's larger
// than the entire range of their axis likely due to some system software bug.
// If we see a very large deadzone, simply ignore the value and use our default.
if (mapping.leftStickDeadzoneRadius > 0.5f) {
mapping.leftStickDeadzoneRadius = 0;
}
// If there isn't a (reasonable) deadzone at all, use 20%
if (mapping.leftStickDeadzoneRadius < 0.02f) {
mapping.leftStickDeadzoneRadius = 0.20f;
}
// Check that the deadzone is 15% at minimum
else if (mapping.leftStickDeadzoneRadius < 0.15f) {
mapping.leftStickDeadzoneRadius = 0.15f;
}
}
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
if (rsXRange != null && rsYRange != null) {
// The flat values should never be negative but we'll deal with it if they are
mapping.rightStickDeadzoneRadius = Math.max(Math.abs(rsXRange.getFlat()),
Math.abs(rsYRange.getFlat()));
// Some devices (certain OUYAs at least) report a deadzone that's larger
// than the entire range of their axis likely due to some system software bug.
// If we see a very large deadzone, simply ignore the value and use our default.
if (mapping.rightStickDeadzoneRadius > 0.5f) {
mapping.rightStickDeadzoneRadius = 0;
}
// If there isn't a (reasonable) deadzone at all, use 20%
if (mapping.rightStickDeadzoneRadius < 0.02f) {
mapping.rightStickDeadzoneRadius = 0.20f;
}
// Check that the deadzone is 15% at minimum
else if (mapping.rightStickDeadzoneRadius < 0.15f) {
mapping.rightStickDeadzoneRadius = 0.15f;
}
}
}
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (mapping.triggerDeadzone < 0.13f ||
mapping.triggerDeadzone > 0.30f)
{
mapping.triggerDeadzone = 0.13f;
}
}
if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should
// use the back button as start since it doesn't have a start/menu button
// on the controller
if (devName.contains("ASUS Gamepad")) {
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
// is Android 5.0 anyway
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) {
mapping.backIsStart = true;
}
}
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
// so we increase the deadzone on them to minimize this
mapping.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks
if (!mapping.hasJoystickAxes) {
mapping.isRemote = true;
}
}
}
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
return mapping;
}
private ControllerMapping getMappingForDevice(InputDevice dev) {
// Unknown devices can't be handled
// Unknown devices use the default mapping
if (dev == null) {
return null;
return defaultMapping;
}
String descriptor = dev.getDescriptor();
@@ -227,9 +269,18 @@ public class ControllerHandler {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
leftStickX, leftStickY, rightStickX, rightStickY);
}
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
if (mapping.isDualShock4) {
// Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
// For remotes, don't capture the back button
if (mapping.isRemote) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1;
}
}
if (mapping.isDualShock4) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1;
@@ -297,8 +348,31 @@ public class ControllerHandler {
return KeyEvent.KEYCODE_DPAD_DOWN;
}
}
// Past here we can fixup the keycode and potentially trigger
// another special case so we need to remember what keycode we're using
int keyCode = event.getKeyCode();
// This is a hack for (at least) the "Tablet Remote" app
// which sends BACK with META_ALT_ON instead of KEYCODE_BUTTON_B
if (keyCode == KeyEvent.KEYCODE_BACK &&
!event.hasNoModifiers() &&
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0)
{
keyCode = KeyEvent.KEYCODE_BUTTON_B;
}
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start
mapping.backIsStart = false;
}
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
// Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START;
}
return event.getKeyCode();
return keyCode;
}
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) {
@@ -309,7 +383,12 @@ public class ControllerHandler {
// Deadzone -- return the zero vector
return Vector2d.ZERO;
}
else {
else {
/*
FIXME: We're not normalizing here because we let the computer handle the deadzones.
Normalizing can make the deadzones larger than they should be after the computer also
evaluates the deadzone
// Scale the input based on the distance from the deadzone
inputVector.getNormalized(normalizedInputVector);
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
@@ -331,80 +410,126 @@ public class ControllerHandler {
}
return normalizedInputVector;
*/
return inputVector;
}
}
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
Vector2d leftStickVector = handleDeadZone(lsX, lsY, mapping.leftStickDeadzoneRadius);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
Vector2d rightStickVector = handleDeadZone(rsX, rsY, mapping.rightStickDeadzoneRadius);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
if (mapping.triggersIdleNegative) {
lt = (lt + 1) / 2;
rt = (rt + 1) / 2;
}
if (lt <= mapping.triggerDeadzone) {
lt = 0;
}
if (rt <= mapping.triggerDeadzone) {
rt = 0;
}
leftTrigger = (byte)(lt * 0xFF);
rightTrigger = (byte)(rt * 0xFF);
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) {
inputMap |= ControllerPacket.LEFT_FLAG;
}
else if (hatX > 0.5) {
inputMap |= ControllerPacket.RIGHT_FLAG;
}
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) {
inputMap |= ControllerPacket.UP_FLAG;
}
else if (hatY > 0.5) {
inputMap |= ControllerPacket.DOWN_FLAG;
}
}
sendControllerInputPacket();
}
public boolean handleMotionEvent(MotionEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
if (mapping == null) {
return false;
}
// Handle left stick events outside of the deadzone
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
Vector2d leftStickVector = handleDeadZone(event.getAxisValue(mapping.leftStickXAxis),
event.getAxisValue(mapping.leftStickYAxis), mapping.leftStickDeadzoneRadius);
leftStickX = (short)(leftStickVector.getX() * 0x7FFE);
leftStickY = (short)(-leftStickVector.getY() * 0x7FFE);
}
// Handle right stick events outside of the deadzone
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
Vector2d rightStickVector = handleDeadZone(event.getAxisValue(mapping.rightStickXAxis),
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
}
// Handle controllers with analog triggers
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
float L2 = event.getAxisValue(mapping.leftTriggerAxis);
float R2 = event.getAxisValue(mapping.rightTriggerAxis);
if (mapping.triggersIdleNegative) {
L2 = (L2 + 1) / 2;
R2 = (R2 + 1) / 2;
}
leftTrigger = (byte)(L2 * 0xFF);
rightTrigger = (byte)(R2 * 0xFF);
}
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// Hats emulate d-pad events
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
// Replay the full history before getting the current values
for (int i = 0; i < event.getHistorySize(); i++) {
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getHistoricalAxisValue(mapping.leftStickXAxis, i);
lsY = event.getHistoricalAxisValue(mapping.leftStickYAxis, i);
}
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -(0.5 + mapping.hatXDeadzone)) {
inputMap |= ControllerPacket.LEFT_FLAG;
}
else if (hatX > (0.5 + mapping.hatXDeadzone)) {
inputMap |= ControllerPacket.RIGHT_FLAG;
}
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -(0.5 + mapping.hatYDeadzone)) {
inputMap |= ControllerPacket.UP_FLAG;
}
else if (hatY > (0.5 + mapping.hatYDeadzone)) {
inputMap |= ControllerPacket.DOWN_FLAG;
}
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
rsX = event.getHistoricalAxisValue(mapping.rightStickXAxis, i);
rsY = event.getHistoricalAxisValue(mapping.rightStickYAxis, i);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
lt = event.getHistoricalAxisValue(mapping.leftTriggerAxis, i);
rt = event.getHistoricalAxisValue(mapping.rightTriggerAxis, i);
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
hatX = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, i);
hatY = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, i);
}
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
}
// Now handle the current set of values
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis);
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis);
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
}
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
sendControllerInputPacket();
return true;
}
public boolean handleButtonUp(int keyCode, KeyEvent event) {
public boolean handleButtonUp(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
if (mapping == null) {
return false;
}
keyCode = handleRemapping(mapping, event);
int keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
@@ -521,13 +646,10 @@ public class ControllerHandler {
return true;
}
public boolean handleButtonDown(int keyCode, KeyEvent event) {
public boolean handleButtonDown(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
if (mapping == null) {
return false;
}
keyCode = handleRemapping(mapping, event);
int keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
@@ -613,8 +735,12 @@ public class ControllerHandler {
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
sendControllerInputPacket();
// Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
sendControllerInputPacket();
}
return true;
}
@@ -630,13 +756,15 @@ public class ControllerHandler {
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public float triggerDeadzone;
public int hatXAxis = -1;
public int hatYAxis = -1;
public float hatXDeadzone;
public float hatYDeadzone;
public boolean isDualShock4;
public boolean isXboxController;
public boolean backIsStart;
public boolean isRemote;
public boolean hasJoystickAxes;
}
}
@@ -12,14 +12,17 @@ public class TouchContext {
private NvConnection conn;
private int actionIndex;
private double xFactor, yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex)
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.xFactor = xFactor;
this.yFactor = yFactor;
}
public int getActionIndex()
@@ -83,8 +86,14 @@ public class TouchContext {
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
conn.sendMouseMove((short)(eventX - lastTouchX),
(short)(eventY - lastTouchY));
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor
deltaX = (int)Math.round((double)deltaX * xFactor);
deltaY = (int)Math.round((double)deltaY * yFactor);
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
lastTouchX = eventX;
@@ -5,7 +5,6 @@ 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;
@@ -19,16 +18,16 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private Thread rendererThread;
private Thread rendererThread, decoderThread;
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 WAIT_CEILING_MS = 5;
private static final int LOW_PERF = 1;
private static final int MED_PERF = 2;
@@ -108,9 +107,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
case LOW_PERF:
// Disable the loop filter for performance reasons
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
// Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1;
@@ -118,8 +115,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
default:
case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
avcFlags = AvcDecoder.BILINEAR_FILTERING;
// Only use 2 threads to minimize frame processing latency
threadCount = 2;
@@ -145,7 +141,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
throw new IllegalStateException("AVC decoder initialization failure: "+err);
}
AvcDecoder.setRenderTarget(sh.getSurface());
if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
return false;
}
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
@@ -156,6 +154,26 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
@Override
public boolean start(final VideoDepacketizer depacketizer) {
decoderThread = new Thread() {
@Override
public void run() {
DecodeUnit du;
while (!isInterrupted()) {
try {
du = depacketizer.takeNextDecodeUnit();
} catch (InterruptedException e) {
break;
}
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
}
};
decoderThread.setName("Video - Decoder (CPU)");
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
decoderThread.start();
rendererThread = new Thread() {
@Override
public void run() {
@@ -163,17 +181,15 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
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;
try {
Thread.sleep(diff - WAIT_CEILING_MS);
} catch (InterruptedException e) {
return;
}
continue;
}
nextFrameTime = computePresentationTimeMs(targetFps);
@@ -194,10 +210,14 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
@Override
public void stop() {
rendererThread.interrupt();
decoderThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException e) { }
rendererThread.join();
} catch (InterruptedException e) { }
try {
decoderThread.join();
} catch (InterruptedException e) { }
}
@Override
@@ -234,7 +254,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
if (delta >= 0 && delta < 1000) {
totalTimeMs += delta;
totalFrames++;
}
@@ -260,4 +280,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
}
return (int)(totalTimeMs / totalFrames);
}
@Override
public String getDecoderName() {
return "CPU decoding";
}
}
@@ -3,9 +3,9 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
private VideoDecoderRenderer decoderRenderer;
private EnhancedDecoderRenderer decoderRenderer;
@Override
public void release() {
@@ -74,4 +74,14 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
return 0;
}
}
@Override
public String getDecoderName() {
if (decoderRenderer != null) {
return decoderRenderer.getDecoderName();
}
else {
return null;
}
}
}
@@ -0,0 +1,7 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
public abstract String getDecoderName();
}
@@ -23,7 +23,7 @@ import android.os.Build;
import android.view.SurfaceHolder;
@SuppressWarnings("unused")
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder;
@@ -32,6 +32,9 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
private VideoDepacketizer depacketizer;
private boolean adaptivePlayback;
private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
private long lastTimestampUs;
private long totalTimeMs;
@@ -63,10 +66,14 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
// Set decoder-specific attributes
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
if (needsSpsBitstreamFixup) {
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
}
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsBaselineSpsHack) {
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
}
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
@@ -266,7 +273,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta > 5 && delta < 300) {
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
@@ -364,7 +371,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
@@ -389,6 +396,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
numIframeIn++;
}
boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
@@ -425,6 +434,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
}
// If we need to hack this SPS to say we're baseline, do so now
if (needsBaselineSpsHack) {
LimeLog.info("Hacking SPS to baseline");
sps.profile_idc = 66;
savedSps = sps;
}
// Write the annex B header
buf.put(header.data, header.offset, 5);
@@ -440,6 +456,14 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
return;
} else if (header.data[header.offset+4] == 0x68) {
numPpsIn++;
if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack");
needsBaselineSpsHack = false;
// Give the decoder the SPS again with the proper profile now
needsSpsReplay = true;
}
}
}
@@ -454,8 +478,39 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
if (needsSpsReplay) {
replaySps();
}
}
private void replaySps() {
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
inputBuffer.clear();
// Write the Annex B header
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
// Switch the H264 profile back to high
savedSps.profile_idc = 100;
// Write the SPS data
savedSps.write(inputBuffer);
// No need for the SPS anymore
savedSps = null;
// Queue the new SPS
queueInputBuffer(inputIndex,
0, inputBuffer.position(),
System.currentTimeMillis() * 1000,
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
LimeLog.info("SPS replay complete");
}
@Override
public int getCapabilities() {
return adaptivePlayback ?
@@ -477,8 +532,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
}
return (int)(totalTimeMs / totalFrames);
}
public class RendererException extends RuntimeException {
@Override
public String getDecoderName() {
return decoderName;
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException;
@@ -25,6 +25,7 @@ public class MediaCodecHelper {
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes;
static {
preferredDecoders = new LinkedList<String>();
@@ -43,7 +44,11 @@ public class MediaCodecHelper {
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
@@ -66,6 +71,10 @@ public class MediaCodecHelper {
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
/*
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
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
@@ -84,7 +93,7 @@ public class MediaCodecHelper {
} catch (Exception e) {
// Tolerate buggy codecs
}
}
}*/
return false;
}
@@ -92,6 +101,10 @@ public class MediaCodecHelper {
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@@ -99,15 +99,17 @@ public class ComputerDatabaseManager {
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
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);
computerList.add(details);
}
c.close();
@@ -151,6 +153,9 @@ public class ComputerDatabaseManager {
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 ||
@@ -1,13 +1,7 @@
package com.limelight.computers;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog;
@@ -26,7 +20,7 @@ import android.os.Binder;
import android.os.IBinder;
public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 5000;
private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private ComputerManagerBinder binder = new ComputerManagerBinder();
@@ -35,9 +29,10 @@ public class ComputerManagerService extends Service {
private AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private HashMap<ComputerDetails, Thread> pollingThreads;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
@@ -62,7 +57,7 @@ public class ComputerManagerService extends Service {
// Returns true if the details object was modified
private boolean runPoll(ComputerDetails details)
{
boolean newPc = (details.name == null);
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) {
return false;
@@ -106,18 +101,9 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
ComputerDetails originalDetails = new ComputerDetails();
originalDetails.update(details);
while (!isInterrupted() && pollingActive) {
// Check if this poll has modified the details
if (runPoll(details) && !originalDetails.equals(details)) {
// Replace our thread entry with the new one
synchronized (pollingThreads) {
pollingThreads.remove(originalDetails);
pollingThreads.put(details, this);
}
}
runPoll(details);
// Wait until the next polling interval
try {
@@ -134,26 +120,24 @@ public class ComputerManagerService extends Service {
public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) {
// Polling is active
pollingActive = true;
// Set the listener
ComputerManagerService.this.listener = listener;
// Start mDNS autodiscovery too
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
// Start polling known machines
if (!getLocalDatabaseReference()) {
return;
}
List<ComputerDetails> computerList = dbManager.getAllComputers();
releaseLocalDatabaseReference();
synchronized (pollingThreads) {
for (ComputerDetails computer : computerList) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// This polling thread might already be there
if (!pollingThreads.containsKey(computer)) {
Thread t = createPollingThread(computer);
pollingThreads.put(computer, t);
t.start();
if (tuple.thread == null) {
// Report this computer initially
listener.notifyComputerUpdated(tuple.computer);
tuple.thread = createPollingThread(tuple.computer);
tuple.thread.start();
}
}
}
@@ -207,11 +191,15 @@ public class ComputerManagerService extends Service {
discoveryBinder.stopDiscovery();
// Stop polling
synchronized (pollingThreads) {
for (Thread t : pollingThreads.values()) {
t.interrupt();
pollingActive = false;
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (tuple.thread != null) {
// Interrupt and remove the thread
tuple.thread.interrupt();
tuple.thread = null;
}
}
pollingThreads.clear();
}
// Remove the listener
@@ -246,29 +234,62 @@ public class ComputerManagerService extends Service {
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Spawn a thread for this computer
synchronized (pollingThreads) {
// This polling thread might already be there
if (!pollingThreads.containsKey(fakeDetails)) {
Thread t = createPollingThread(fakeDetails);
pollingThreads.put(fakeDetails, t);
t.start();
addTuple(fakeDetails);
}
private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer == details ||
// If there's no name on one of these computers, compare with the local IP
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
tuple.computer.localIp.equals(details.localIp)) ||
// If there is a name on both computers, compare with name
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
tuple.computer.name.equals(details.name))) {
// Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) {
tuple.thread = createPollingThread(details);
tuple.thread.start();
}
// Found an entry so we're done
return;
}
}
// If we got here, we didn't find an entry
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
pollingTuples.add(tuple);
if (tuple.thread != null) {
tuple.thread.start();
}
}
}
}
public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details
runPoll(fakeDetails);
// If the machine is reachable, it was successful
return fakeDetails.state == ComputerDetails.State.ONLINE;
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
// Start a polling thread for this machine
addTuple(fakeDetails);
return true;
}
else {
return false;
}
}
public void removeComputer(String name) {
@@ -278,6 +299,20 @@ public class ComputerManagerService extends Service {
// Remove it from the database
dbManager.deleteComputer(name);
synchronized (pollingTuples) {
// Remove the computer from the computer list
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.name.equals(name)) {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
}
pollingTuples.remove(tuple);
break;
}
}
}
releaseLocalDatabaseReference();
}
@@ -297,12 +332,22 @@ public class ComputerManagerService extends Service {
}
}
private ComputerDetails tryPollIp(InetAddress ipAddr) {
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
return http.getComputerDetails();
ComputerDetails newDetails = http.getComputerDetails();
// Check if this is the PC we expected
if (details.uuid != null && newDetails.uuid != null &&
!details.uuid.equals(newDetails.uuid)) {
// We got the wrong PC!
LimeLog.info("Polling returned the wrong PC!");
return null;
}
return newDetails;
} catch (Exception e) {
return null;
}
@@ -312,19 +357,19 @@ public class ComputerManagerService extends Service {
ComputerDetails polledDetails;
if (localFirst) {
polledDetails = tryPollIp(details.localIp);
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details.remoteIp);
polledDetails = tryPollIp(details, details.remoteIp);
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details.localIp);
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details.remoteIp);
polledDetails = tryPollIp(details, details.remoteIp);
}
// The fallback poll worked
@@ -349,7 +394,18 @@ public class ComputerManagerService extends Service {
}
private boolean doPollMachine(ComputerDetails details) {
return pollComputer(details, true);
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Always try local first to avoid potential UDP issues when
// attempting to stream via the router's external IP address
// behind its NAT
return pollComputer(details, true);
}
else {
// If we're already reached a machine via a particular IP address,
// always try that one first
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
}
}
@Override
@@ -357,8 +413,6 @@ public class ComputerManagerService extends Service {
// Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
pollingThreads = new HashMap<ComputerDetails, Thread>();
// Lookup or generate this device's UID
idManager = new IdentityManager(this);
@@ -366,6 +420,18 @@ public class ComputerManagerService extends Service {
// Initialize the DB
dbManager = new ComputerDatabaseManager(this);
dbRefCount.set(1);
// Grab known machines into our computer list
if (!getLocalDatabaseReference()) {
return;
}
for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer
addTuple(computer);
}
releaseLocalDatabaseReference();
}
@Override
@@ -386,3 +452,13 @@ public class ComputerManagerService extends Service {
return binder;
}
}
class PollingTuple {
public Thread thread;
public ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
this.thread = thread;
}
}
@@ -0,0 +1,169 @@
package com.limelight.grid;
import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion;
import com.limelight.AppView;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.LimelightCryptoProvider;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private InetAddress address;
private String uniqueId;
private LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, R.layout.app_grid_item, R.drawable.image_loading);
this.address = address;
this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context);
sslContext = SSLContext.getInstance("SSL");
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
}
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) {
return "Limelight-RSA";
}
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket) {
return null;
}
public X509Certificate[] getCertificateChain(String alias) {
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
}
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
public PrivateKey getPrivateKey(String alias) {
return cryptoProvider.getClientPrivateKey();
}
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
}
};
// Ignore differences between given hostname and certificate hostname
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
};
public void addApp(AppView.AppObject app) {
itemList.add(app);
}
public void abortPendingRequests() {
HashMap<ImageView, Future> tempMap;
synchronized (pendingRequests) {
// Copy the pending requests under a lock
tempMap = new HashMap<ImageView, Future>(pendingRequests);
}
for (Future f : tempMap.values()) {
f.cancel(true);
}
synchronized (pendingRequests) {
// Remove cancelled requests
for (ImageView v : tempMap.keySet()) {
pendingRequests.remove(v);
}
}
}
@Override
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) {
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
// Set off the deferred image load
synchronized (pendingRequests) {
Future f = Ion.with(imgView)
.placeholder(defaultImageRes)
.error(defaultImageRes)
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
.setCallback(new FutureCallback<ImageView>() {
@Override
public void onCompleted(Exception e, ImageView result) {
synchronized (pendingRequests) {
pendingRequests.remove(imgView);
}
}
});
pendingRequests.put(imgView, f);
}
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.play);
return true;
}
// No overlay
return false;
}
}
@@ -0,0 +1,78 @@
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.TextView;
import com.limelight.R;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected Context context;
protected int defaultImageRes;
protected int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context;
this.layoutId = layoutId;
this.defaultImageRes = defaultImageRes;
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);
@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);
if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes);
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
return convertView;
}
}
@@ -0,0 +1,62 @@
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;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context) {
super(context, R.layout.pc_grid_item, R.drawable.computer);
}
public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer);
}
public boolean removeComputer(PcView.ComputerObject computer) {
return itemList.remove(computer);
}
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
// Return false to use the default drawable
return false;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
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.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Still refreshing this PC so display the overlay
overlayView.setImageResource(R.drawable.image_loading);
return true;
}
// No overlay
return false;
}
}
@@ -7,6 +7,7 @@ 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;
@@ -16,14 +17,12 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private Button addPcButton;
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
@@ -43,21 +42,27 @@ public class AddComputerManually extends Activity {
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 = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
msg = getResources().getString(R.string.addpc_fail);
}
else {
msg = "Successfully added computer";
msg = getResources().getString(R.string.addpc_success);
finish = true;
}
} catch (UnknownHostException e) {
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
msg = getResources().getString(R.string.addpc_unknown_host);
}
final boolean toastFinish = finish;
dialog.dismiss();
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
@@ -110,6 +115,7 @@ public class AddComputerManually extends Activity {
super.onStop();
Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this);
}
@Override
@@ -130,24 +136,29 @@ public class AddComputerManually extends Activity {
UiHelper.notifyNewRootView(this);
this.addPcButton = (Button) findViewById(R.id.addPc);
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());
}
return false;
}
});
// 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());
}
});
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
}
}
@@ -12,6 +12,7 @@ public class PreferenceConfiguration {
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 int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -25,6 +26,7 @@ public class PreferenceConfiguration {
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 int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
@@ -33,6 +35,7 @@ public class PreferenceConfiguration {
public int width, height, fps;
public int bitrate;
public int decoder;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public static int getDefaultBitrate(String resFpsString) {
@@ -130,6 +133,8 @@ public class PreferenceConfiguration {
config.decoder = getDecoderValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
// Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
@@ -19,11 +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 splashText, valueText;
private TextView valueText;
private Context context;
private String dialogMessage, suffix;
private int defaultValue, maxValue, currentValue;
private int defaultValue, maxValue, minValue, currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -47,9 +47,10 @@ public class SeekBarPreference extends DialogPreference
suffix = context.getString(suffixId);
}
// Get default and max seekbar values
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
// Get default, min, and max seekbar values
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
}
@Override
@@ -60,7 +61,7 @@ public class SeekBarPreference extends DialogPreference
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6);
splashText = new TextView(context);
TextView splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) {
splashText.setText(dialogMessage);
@@ -71,7 +72,7 @@ public class SeekBarPreference extends DialogPreference
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.FILL_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params);
@@ -79,8 +80,13 @@ 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));
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
}
@Override
@@ -90,7 +96,7 @@ public class SeekBarPreference extends DialogPreference
public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue);
@@ -149,10 +155,10 @@ public class SeekBarPreference extends DialogPreference
if (shouldPersist()) {
currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress());
callChangeListener(Integer.valueOf(seekBar.getProgress()));
callChangeListener(seekBar.getProgress());
}
((AlertDialog) getDialog()).dismiss();
getDialog().dismiss();
}
});
}
@@ -14,7 +14,7 @@ public class UiHelper {
public static void notifyNewRootView(Activity activity)
{
View rootView = (View) activity.findViewById(android.R.id.content);
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)
+12 -12
View File
@@ -69,9 +69,6 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
return -1;
}
// Show frames even before a reference frame
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
if (perf_lvl & DISABLE_LOOP_FILTER) {
// Skip the loop filter for performance reasons
decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
@@ -370,17 +367,20 @@ int nv_avc_decode(unsigned char* indata, int inlen) {
// Only copy the picture at the end of decoding the packet
if (got_pic) {
// Clone the current decode frame outside of the mutex
AVFrame* new_frame = av_frame_clone(dec_frame);
AVFrame* old_frame;
// Swap it in under lock
pthread_mutex_lock(&mutex);
// Only clone this frame if the last frame was taken.
// This saves on extra copies for frames that don't get
// rendered.
if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
}
old_frame = yuv_frame;
yuv_frame = new_frame;
pthread_mutex_unlock(&mutex);
// Free the old frame outside of the mutex
if (old_frame != NULL) {
av_frame_free(&old_frame);
}
}
return err < 0 ? err : 0;
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<stroke android:width="1dip" android:color="#ffffff"/>
</shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -8,48 +8,69 @@
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" >
<ListView
android:id="@+id/pcListView"
android:layout_width="match_parent"
<RelativeLayout
android:id="@+id/no_pc_found_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/searching_pc"/>
</RelativeLayout>
<GridView
android:id="@+id/pcGridView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/settingsButton"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc"
android:layout_toRightOf="@+id/settingsButton"
android:layout_toEndOf="@+id/settingsButton"/>
</ListView>
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_centerHorizontal="true"
android:layout_alignBaseline="@+id/settingsButton"
android:text="@string/title_pc_view" />
<Button
<ImageButton
android:id="@+id/settingsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/pcListView"
android:layout_width="70dp"
android:layout_height="65dp"
android:cropToPadding="false"
android:scaleType="fitXY"
android:nextFocusDown="@+id/pcGridView"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:text="@string/button_stream_settings" />
<Button
android:id="@+id/manuallyAddPc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@+id/pcListView"
android:layout_alignParentTop="true"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:text="@string/button_add_pc_manually" />
android:src="@drawable/settings"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout>
<ImageButton
android:id="@+id/manuallyAddPc"
android:layout_width="70dp"
android:layout_height="65dp"
android:cropToPadding="false"
android:scaleType="fitXY"
android:nextFocusDown="@+id/pcGridView"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:src="@drawable/add_computer"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout>
@@ -8,46 +8,67 @@
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" >
<ListView
android:id="@+id/pcListView"
android:layout_width="match_parent"
<RelativeLayout
android:id="@+id/no_pc_found_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/searching_pc"/>
</RelativeLayout>
<GridView
android:id="@+id/pcGridView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center"
android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc"
android:layout_toRightOf="@+id/settingsButton"
android:layout_toEndOf="@+id/settingsButton"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/discoveryText"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
android:layout_alignParentTop="true"/>
</ListView>
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_below="@+id/manuallyAddPc"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/title_pc_view" />
<Button
<ImageButton
android:id="@+id/settingsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_width="70dp"
android:layout_height="65dp"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/button_stream_settings" />
android:src="@drawable/settings"
style="?android:attr/borderlessButtonStyle"/>
<Button
<ImageButton
android:id="@+id/manuallyAddPc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/settingsButton"
android:layout_centerHorizontal="true"
android:text="@string/button_add_pc_manually" />
android:layout_width="70dp"
android:layout_height="65dp"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:src="@drawable/add_computer"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout>
</RelativeLayout>
@@ -5,16 +5,28 @@
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="10dp"
tools:context=".Connection" >
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AddComputerManually" >
<TextView
android:id="@+id/manuallyAddPcText"
android:text="@string/title_add_pc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true"/>
<EditText
android:id="@+id/hostTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText"
android:layout_marginTop="25dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:ems="10"
android:singleLine="true"
android:inputType="textNoSuggestions"
@@ -22,13 +34,5 @@
<requestFocus />
</EditText>
<Button
android:id="@+id/addPc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/hostTextView"
android:layout_centerHorizontal="true"
android:text="@string/button_add_pc" />
</RelativeLayout>
+13 -11
View File
@@ -8,28 +8,30 @@
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" >
<ListView
android:id="@+id/pcListView"
android:layout_width="match_parent"
<GridView
android:id="@+id/appGridView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:gravity="center"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText"
android:fastScrollEnabled="true"
android:longClickable="false"
android:background="@drawable/list_view_unselected"
android:stackFromBottom="false">
</ListView>
android:layout_below="@+id/appListText">
</GridView>
<TextView
android:id="@+id/appListText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true"
android:paddingTop="0dp"
android:paddingBottom="10dp" />
android:paddingBottom="10dp"
android:textSize="28sp"/>
</RelativeLayout>
@@ -2,7 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
tools:context=".Game" >
<SurfaceView
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="175dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="18sp" >
</TextView>
</RelativeLayout>
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="100dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="15dp"
android:layout_marginLeft="65dp"
android:layout_marginStart="65dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="18sp" >
</TextView>
</RelativeLayout>
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:padding="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/rowTextView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textIsSelectable="false"
android:textSize="16sp" >
</TextView>
</LinearLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>720p - 30 FPS</item>
<item>720p - 60 FPS</item>
<item>1080p - 30 FPS</item>
<item>1080p - 60 FPS</item>
</string-array>
<string-array name="decoder_names">
<item>Scegli decoder automaticamente</item>
<item>Forza decoder software</item>
<item>Forza decoder hardware</item>
</string-array>
</resources>
+106
View File
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Lista applicazioni</string>
<string name="pcview_menu_pair_pc">Accoppia PC</string>
<string name="pcview_menu_unpair_pc">Disaccoppia PC</string>
<string name="pcview_menu_send_wol">Invia richiesta Wake-On-LAN</string>
<string name="pcview_menu_delete_pc">Rimuovi PC</string>
<!-- Pair messages -->
<string name="pairing">Accoppiamento…</string>
<string name="pair_pc_offline">PC offline</string>
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
<string name="pair_pairing_title">Accoppiamento</string>
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
<string name="pair_incorrect_pin">PIN non corretto</string>
<string name="pair_fail">Accoppiamento fallito</string>
<!-- WOL messages -->
<string name="wol_pc_online">PC già avviato</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_waking_pc">Risveglio PC…</string>
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
</string>
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
<!-- Unpair messages -->
<string name="unpairing">Disaccoppiamento…</string>
<string name="unpair_success">Disaccoppiato con successo</string>
<string name="unpair_fail">Disaccoppiamento fallito</string>
<string name="unpair_error">PC non accoppiato</string>
<!-- Errors -->
<string name="error_pc_offline">PC offline</string>
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
<string name="error_unknown_host">Risoluzione nome host fallita</string>
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Connessione</string>
<string name="conn_establishing_msg">Connessione in corso</string>
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
<string name="conn_client_latency">Latenza frame media client-side:</string>
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
<string name="conn_starting">Avvio in corso…</string>
<string name="conn_error_title">Errore connessione</string>
<string name="conn_error_msg">Avvio fallito</string>
<string name="conn_terminated_title">Connessione terminata</string>
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
<!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string>
<!-- AppList activity -->
<string name="title_applist">Applicazioni su</string>
<string name="applist_menu_resume">Riprendi Sessione</string>
<string name="applist_menu_quit">Chiudi Sessione</string>
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
<string name="applist_menu_cancel">Annulla</string>
<string name="applist_refresh_title">Lista applicazioni</string>
<string name="applist_refresh_msg">Aggiornamento lista in corso…</string>
<string name="applist_refresh_error_title">Errore</string>
<string name="applist_refresh_error_msg">Ricezione lista applicazioni fallita</string>
<string name="applist_quit_app">Chiusura in corso…</string>
<string name="applist_quit_success">Sessione chiusa con successo</string>
<string name="applist_quit_fail">Chiusura sessione fallita</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string>
<string name="msg_add_pc">Connessione al PC in corso…</string>
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
<string name="addpc_success">PC aggiunto con successo</string>
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
<!-- Preferences -->
<string name="category_basic_settings">Impostazioni Base</string>
<string name="title_resolution_list">Risoluzione e FPS</string>
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
<string name="title_seekbar_bitrate">Bitrate video</string>
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_disable_warnings">Disabilita i messaggi di warning</string>
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
<string name="category_gamepad_settings">Impostazioni Gamepad</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="category_host_settings">Impostazioni Host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo. Richiede GFE 2.1.2+</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="title_decoder_list">Cambia decoder</string>
<string name="summary_decoder_list">Il decoder software può aumentare la latenza video quando si usano impostazioni streaming basse</string>
</resources>
+1 -1
View File
@@ -3,7 +3,7 @@
<!--
Base application theme for API 21+. This theme completely replaces
AppBaseTheme from BOTH res/values/styles.xml and
res/values-v11/styles.xml on API 21+ devices.
res/values-v21/styles.xml on API 21+ devices.
-->
<style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. -->
+5 -5
View File
@@ -6,7 +6,7 @@
<item>1080p 30 FPS</item>
<item>1080p 60 FPS</item>
</string-array>
<string-array name="resolution_values">
<string-array name="resolution_values" translatable="false">
<item>720p30</item>
<item>720p60</item>
<item>1080p30</item>
@@ -14,13 +14,13 @@
</string-array>
<string-array name="decoder_names">
<item>Force Software Decoding</item>
<item>Auto-select Decoder</item>
<item>Force Software Decoding</item>
<item>Force Hardware Decoding</item>
</string-array>
<string-array name="decoder_values">
<item>software</item>
<string-array name="decoder_values" translatable="false">
<item>auto</item>
<item>software</item>
<item>hardware</item>
</string-array>
</resources>
</resources>
+77 -5
View File
@@ -1,16 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">View Game List</string>
<string name="pcview_menu_pair_pc">Pair with PC</string>
<string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string>
<!-- Pair messages -->
<string name="pairing">Pairing…</string>
<string name="pair_pc_offline">Computer is offline</string>
<string name="pair_pc_ingame">Computer is currently in a game. You must close the game before pairing.</string>
<string name="pair_pairing_title">Pairing</string>
<string name="pair_pairing_msg">Please enter the following PIN on the target PC:</string>
<string name="pair_incorrect_pin">Incorrect PIN</string>
<string name="pair_fail">Pairing failed</string>
<!-- WOL messages -->
<string name="wol_pc_online">Computer is online</string>
<string name="wol_no_mac">Unable to wake PC because GFE didn\'t send a MAC address</string>
<string name="wol_waking_pc">Waking PC…</string>
<string name="wol_waking_msg">It may take a few seconds for your PC to wake up.
If it doesn\'t, make sure it\'s configured properly for Wake-On-LAN.
</string>
<string name="wol_fail">Failed to send Wake-On-LAN packets</string>
<!-- Unpair messages -->
<string name="unpairing">Unpairing…</string>
<string name="unpair_success">Unpaired successfully</string>
<string name="unpair_fail">Failed to unpair</string>
<string name="unpair_error">Device was not paired</string>
<!-- Errors -->
<string name="error_pc_offline">Computer is offline</string>
<string name="error_manager_not_running">The ComputerManager service is not running. Please wait a few seconds or restart the app.</string>
<string name="error_unknown_host">Failed to resolve host</string>
<string name="error_404">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.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
<string name="conn_establishing_msg">Starting connection</string>
<string name="conn_metered">Warning: Your active network connection is metered!</string>
<string name="conn_client_latency">Average client-side frame latency:</string>
<string name="conn_client_latency_hw">hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
<string name="conn_starting">Starting</string>
<string name="conn_error_title">Connection Error</string>
<string name="conn_error_msg">Failed to start</string>
<string name="conn_terminated_title">Connection Terminated</string>
<string name="conn_terminated_msg">The connection was terminated</string>
<!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string>
<!-- PC view activity -->
<string name="title_pc_view">PC List</string>
<string name="button_stream_settings">Streaming Settings</string>
<string name="button_add_pc_manually">Add PC Manually</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_menu_resume">Resume Session</string>
<string name="applist_menu_quit">Quit Session</string>
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
<string name="applist_menu_cancel">Cancel</string>
<string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string>
<string name="applist_refresh_error_msg">Failed to get app list</string>
<string name="applist_quit_app">Quitting</string>
<string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string>
<!-- Add computer manually activity -->
<string name="button_add_pc">Manually Add PC</string>
<string name="title_add_pc">Add PC Manually</string>
<string name="msg_add_pc">Connecting to the PC…</string>
<string name="addpc_fail">Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.</string>
<string name="addpc_success">Successfully added computer</string>
<string name="addpc_unknown_host">Unable to resolve PC address. Make sure you didn\'t make a typo in the address.</string>
<string name="addpc_enter_ip">You must enter an IP address</string>
<!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string>
@@ -23,6 +90,10 @@
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string>
<string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
@@ -31,4 +102,5 @@
<string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</string>
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
</resources>
+10
View File
@@ -22,4 +22,14 @@
<item name="android:windowNoTitle">true</item>
</style>
<!-- Stream activity theme -->
<style name="StreamTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<!-- Transparent streaming background to avoid extra overdraw -->
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>
+9
View File
@@ -26,6 +26,14 @@
android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" />
</PreferenceCategory>
<!--PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
android:max="50"
android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/>
</PreferenceCategory-->
<PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference
android:key="checkbox_enable_sops"
@@ -44,6 +52,7 @@
android:title="@string/title_decoder_list"
android:entries="@array/decoder_names"
android:entryValues="@array/decoder_values"
android:summary="@string/summary_decoder_list"
android:defaultValue="auto" />
</PreferenceCategory>
+9 -3
View File
@@ -4,16 +4,22 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- Affected decoders: TI OMAP4, Allwinner A20
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4
- Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
- Affected decoders: TI OMAP4
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
5. Some decoders will hang if max_dec_frame_buffering is not present
- Affected decoders: MediaTek decoder in Fire HD 7 (2014)
6. Some decoders will hang if max_dec_frame_buffering IS present
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
7. Some decoders will not enter low latency mode if adaptive playback is enabled
- Affected decoders: Intel decoder in Nexus Player
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS.
- Affected decoders: Intel decoder in Nexus Player