Compare commits

..
2 Commits
Author SHA1 Message Date
Cameron Gutman 2ba3c0e477 Update common again 2014-11-11 01:09:45 -08:00
Cameron Gutman fe27957000 Update 2.9 series to support GFE 2.1.4 2014-11-10 22:09:56 -08:00
94 changed files with 1166 additions and 4165 deletions

No files matched your search

+9 -9
View File
@@ -1,14 +1,12 @@
#Moonlight #Limelight
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield. Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device. We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device, Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet. whether in your own home or over the internet.
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development. [Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
##Features ##Features
@@ -18,7 +16,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for
##Installation ##Installation
* Download and install Moonlight for Android from * Download and install Limelight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight) [Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC * Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
@@ -33,7 +31,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for
* Turn on GameStream in the GFE settings * Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet * If you are connecting from outside the same network, turn on internet
streaming streaming
* When on the same network as your PC, open Moonlight and tap on your PC in the list * When on the same network as your PC, open Limelight and tap on your PC in the list
* Accept the pairing confirmation on your PC * Accept the pairing confirmation on your PC
* Tap your PC again to view the list of apps to stream * Tap your PC again to view the list of apps to stream
* Play games! * Play games!
@@ -46,6 +44,8 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code 2. Write code
3. Send Pull Requests 3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors ##Authors
* [Cameron Gutman](https://github.com/cgutman) * [Cameron Gutman](https://github.com/cgutman)
@@ -53,5 +53,5 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
* [Aaron Neyer](https://github.com/Aaronneyer) * [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername) * [Andrew Hennessy](https://github.com/yetanothername)
Moonlight is the work of students at [Case Western](http://case.edu) and was Limelight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org). started as a project at [MHacks](http://mhacks.org).
+18 -24
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle"> <facet type="android-gradle" name="Android-Gradle">
<configuration> <configuration>
@@ -9,15 +9,11 @@
<facet type="android" name="Android"> <facet type="android" name="Android">
<configuration> <configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" /> <option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" /> <option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" /> <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" /> <option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<afterSyncTasks> <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<task>generateNonRootDebugAndroidTestSources</task>
<task>generateNonRootDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" /> <option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -26,9 +22,8 @@
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" /> <output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -36,7 +31,6 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
@@ -44,12 +38,11 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -106,15 +99,16 @@
<excludeFolder url="file://$MODULE_DIR$/build/outputs" /> <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" /> <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content> </content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" /> <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" /> <orderEntry type="library" exported="" name="okhttp-2.1.0-RC1" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" /> <orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" /> <orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" /> <orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" /> <orderEntry type="library" exported="" name="okio-1.0.1" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
</component> </component>
</module> </module>
+12 -15
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 23 compileSdkVersion 21
buildToolsVersion "22.0.1" buildToolsVersion "21.0.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 23 targetSdkVersion 21
versionName "3.1.9" versionName "2.9.0.1"
versionCode = 64 versionCode = 40
} }
productFlavors { productFlavors {
@@ -27,7 +27,7 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled false runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
} }
@@ -62,15 +62,12 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9' 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: 'bcprov-jdk15on', version: '1.52' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52' compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1'
compile group: 'com.squareup.okio', name:'okio', version:'1.0.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0' compile files('libs/jmdns-fixed.jar')
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
compile files('libs/jmdns-3.4.2.jar')
compile files('libs/limelight-common.jar') compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar') compile files('libs/tinyrtsp.jar')
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -7
View File
@@ -17,10 +17,6 @@
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme" > android:theme="@style/AppTheme" >
<!-- Samsung multi-window support -->
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<!-- Launcher for traditional devices --> <!-- Launcher for traditional devices -->
<activity <activity
android:name=".PcView" android:name=".PcView"
@@ -28,7 +24,6 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" /> <category android:name="tv.ouya.intent.category.APP" />
</intent-filter> </intent-filter>
</activity> </activity>
@@ -69,11 +64,10 @@
<activity <activity
android:name=".Game" android:name=".Game"
android:screenOrientation="sensorLandscape" android:screenOrientation="sensorLandscape"
android:theme="@style/StreamTheme"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" > android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" /> android:value="com.limelight.Connection" />
</activity> </activity>
<service <service
android:name=".discovery.DiscoveryService" android:name=".discovery.DiscoveryService"
+160 -367
View File
@@ -1,239 +1,101 @@
package com.limelight; package com.limelight;
import java.io.StringReader; import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.computers.ComputerManagerListener; import org.xmlpull.v1.XmlPullParserException;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.R;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity implements AdapterFragmentCallbacks { public class AppView extends Activity {
private AppGridAdapter appGridAdapter; private ListView appList;
private String uuidString; private ArrayAdapter<AppObject> appListAdapter;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private ComputerDetails computer; private final static int RESUME_ID = 1;
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3; private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4;
public final static String ADDRESS_EXTRA = "Address";
public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name"; public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID"; public final static String REMOTE_EXTRA = "Remote";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view); setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this); byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
return;
}
uuidString = getIntent().getStringExtra(UUID_EXTRA); 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); TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
}
private void populateAppGridWithCache() {
try { try {
// Try to load from cache ipAddress = InetAddress.getByAddress(address);
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString)); } catch (UnknownHostException e) {
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist)); return;
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
}
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
} }
private void loadAppsBlocking() { // Setup the list view
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title), appList = (ListView)findViewById(R.id.pcListView);
getResources().getString(R.string.applist_refresh_msg), true); appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
appListAdapter.setNotifyOnChange(false);
appList.setAdapter(appListAdapter);
appList.setItemsCanFocus(true);
appList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
}
else {
doStart(app.app);
}
}
});
registerForContextMenu(appList);
} }
@Override @Override
@@ -242,30 +104,23 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
startComputerUpdates(); updateAppList();
}
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates();
} }
private int getRunningAppId() { private int getRunningAppId() {
int runningAppId = -1; int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) { for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i); AppObject app = appListAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) { if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId(); runningAppId = app.app.getAppId();
break; break;
@@ -279,16 +134,20 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position); AppObject selectedApp = appListAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId(); int runningAppId = getRunningAppId();
if (runningAppId != -1) { if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) { if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
} }
else { else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
} }
} }
} }
@@ -300,43 +159,16 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position); AppObject app = appListAdapter.getItem(info.position);
switch (item.getItemId()) { switch (item.getItemId())
case START_WTIH_QUIT: {
// Display a confirmation dialog first case RESUME_ID:
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us // Resume is the same as start for us
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder); doStart(app.app);
return true; return true;
case QUIT_ID: case QUIT_ID:
// Display a confirmation dialog first doQuit(app.app);
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
}
}, null);
return true; return true;
case CANCEL_ID: case CANCEL_ID:
@@ -347,156 +179,117 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
} }
} }
private void updateUiWithServerinfo(final ComputerDetails details) { 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);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() { AppView.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
boolean updated = false; appListAdapter.clear();
if (appList.isEmpty()) {
// Look through our current app list to tag the running app addListPlaceholder();
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
updated = true;
}
else if (existingApp.app.getIsRunning()) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
updated = true;
} }
else { else {
// This app wasn't running and still isn't for (NvApp app : appList) {
appListAdapter.add(new AppObject(generateString(app), app));
} }
} }
if (updated) { appListAdapter.notifyDataSetChanged();
appGridAdapter.notifyDataSetChanged();
}
} }
}); });
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
} }
private void updateUiWithAppList(final List<NvApp> appList) { Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
AppView.this.runOnUiThread(new Runnable() { }
}.start();
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
boolean updated = false; NvHTTP httpConn;
String message;
// First handle app updates and additions try {
for (NvApp app : appList) { httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
boolean foundExistingApp = false; if (httpConn.quitApp()) {
message = "Successfully quit "+app.getAppName();
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunning(app.getIsRunning());
updated = true;
} }
if (!existingApp.app.getAppName().equals(app.getAppName())) { else {
existingApp.app.setAppName(app.getAppName()); message = "Failed to quit "+app.getAppName();
updated = true; }
updateAppList();
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
} }
foundExistingApp = true; final String toastMessage = message;
break; runOnUiThread(new Runnable() {
} @Override
} public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
// Next handle app removals
int i = 0;
while (i < appGridAdapter.getCount()) {
boolean foundExistingApp = false;
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// Check if this app is in the latest list
for (NvApp app : appList) {
if (existingApp.app.getAppId() == app.getAppId()) {
foundExistingApp = true;
break;
}
}
// This app was removed in the latest app list
if (!foundExistingApp) {
appGridAdapter.removeApp(existingApp);
updated = true;
// Check this same index again because the item at i+1 is now at i after
// the removal
continue;
}
// Move on to the next item
i++;
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
} }
}); });
} }
}).start();
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(appGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
} }
public class AppObject { public class AppObject {
public final NvApp app; public String text;
public NvApp app;
public AppObject(NvApp app) { public AppObject(String text, NvApp app) {
if (app == null) { this.text = text;
throw new IllegalArgumentException("app must not be null");
}
this.app = app; this.app = app;
} }
@Override @Override
public String toString() { public String toString() {
return app.getAppName(); return text;
} }
} }
} }
+32 -117
View File
@@ -1,7 +1,6 @@
package com.limelight; package com.limelight;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.input.ControllerHandler; import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator; import com.limelight.binding.input.KeyboardTranslator;
@@ -13,26 +12,21 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration; import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket; import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point; import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import android.view.Display; import android.view.Display;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -46,31 +40,25 @@ import android.view.View.OnTouchListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; import android.widget.Toast;
import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback, public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures OnSystemUiVisibilityChangeListener
{ {
private int lastMouseX = Integer.MIN_VALUE; private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE; private int lastMouseY = Integer.MIN_VALUE;
private int lastButtonState = 0; private int lastButtonState = 0;
// Only 2 touches are supported // Only 2 touches are supported
private final TouchContext[] touchContextMap = new TouchContext[2]; private TouchContext[] touchContextMap = new TouchContext[2];
private long threeFingerDownTime = 0;
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
private ControllerHandler controllerHandler; private ControllerHandler controllerHandler;
private KeyboardTranslator keybTranslator; private KeyboardTranslator keybTranslator;
private PreferenceConfiguration prefConfig; private PreferenceConfiguration prefConfig;
private final Point screenSize = new Point(0, 0); private Point screenSize = new Point(0, 0);
private NvConnection conn; private NvConnection conn;
private SpinnerDialog spinner; private SpinnerDialog spinner;
@@ -90,8 +78,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int drFlags = 0; private int drFlags = 0;
public static final String EXTRA_HOST = "Host"; public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP_NAME = "AppName"; public static final String EXTRA_APP = "App";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId"; public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote"; public static final String EXTRA_STREAMING_REMOTE = "Remote";
@@ -99,13 +86,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// We don't want a title bar // We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -135,8 +115,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
setContentView(R.layout.activity_game); setContentView(R.layout.activity_game);
// Start the spinner // Start the spinner
spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title), spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
getResources().getString(R.string.conn_establishing_msg), true);
// Read the stream preferences // Read the stream preferences
prefConfig = PreferenceConfiguration.readPreferences(this); prefConfig = PreferenceConfiguration.readPreferences(this);
@@ -173,15 +152,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.acquire(); wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST); String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME); String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
decoderRenderer = new ConfigurableDecoderRenderer(); decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags); decoderRenderer.initializeWithFlags(drFlags);
@@ -189,23 +161,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
StreamConfiguration config = new StreamConfiguration.Builder() StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height) .setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps) .setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId)) .setApp(app)
.setBitrate(prefConfig.bitrate * 1000) .setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops) .setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() & .enableAdaptiveResolution((decoderRenderer.getCapabilities() &
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0) VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
.enableLocalAudioPlayback(prefConfig.playHostAudio) .enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292)
.setRemote(remote)
.build(); .build();
// Initialize the connection // Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this)); conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn); keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage); controllerHandler = new ControllerHandler(conn);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
SurfaceHolder sh = sv.getHolder(); SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
@@ -215,9 +182,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize touch contexts // Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) { for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i, touchContextMap[i] = new TouchContext(conn, i);
((double)prefConfig.width / (double)screenSize.x),
((double)prefConfig.height / (double)screenSize.y));
} }
if (LimelightBuildProps.ROOT_BUILD) { if (LimelightBuildProps.ROOT_BUILD) {
@@ -249,12 +214,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{ {
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (mgr.isActiveNetworkMetered()) { if (mgr.isActiveNetworkMetered()) {
displayTransientMessage(getResources().getString(R.string.conn_metered)); displayTransientMessage("Warning: Your active network connection is metered!");
} }
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private final Runnable hideSystemUi = new Runnable() { private Runnable hideSystemUi = new Runnable() {
@Override @Override
public void run() { public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds // Use immersive mode on 4.4+ or standard low profile on previous builds
@@ -290,9 +255,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
displayedFailureDialog = true; displayedFailureDialog = true;
stopConnection(); stopConnection();
@@ -300,13 +262,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency(); int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null; String message = null;
if (averageEndToEndLat > 0) { if (averageEndToEndLat > 0) {
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms"; message = "Average client-side frame latency: "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) { if (averageDecoderLat > 0) {
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)"; message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
} }
} }
else if (averageDecoderLat > 0) { else if (averageDecoderLat > 0) {
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms"; message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
} }
if (message != null) { if (message != null) {
@@ -323,7 +285,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.release(); wifiLock.release();
} }
private final Runnable toggleGrab = new Runnable() { private Runnable toggleGrab = new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -421,13 +383,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyDown(keyCode, event);
}
// Pass-through virtual navigation keys // Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) { if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyDown(keyCode, event); return super.onKeyDown(keyCode, event);
} }
// Try the controller handler first // Try the controller handler first
boolean handled = controllerHandler.handleButtonDown(event); boolean handled = controllerHandler.handleButtonDown(keyCode, event);
if (!handled) { if (!handled) {
// Try the keyboard handler // Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode()); short translated = keybTranslator.translate(event.getKeyCode());
@@ -459,13 +426,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyUp(keyCode, event);
}
// Pass-through virtual navigation keys // Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) { if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyUp(keyCode, event); return super.onKeyUp(keyCode, event);
} }
// Try the controller handler first // Try the controller handler first
boolean handled = controllerHandler.handleButtonUp(event); boolean handled = controllerHandler.handleButtonUp(keyCode, event);
if (!handled) { if (!handled) {
// Try the keyboard handler // Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode()); short translated = keybTranslator.translate(event.getKeyCode());
@@ -499,13 +471,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
@Override
public void showKeyboard() {
LimeLog.info("Showing keyboard overlay");
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
// Returns true if the event was consumed // Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) { private boolean handleMotionEvent(MotionEvent event) {
// Pass through keyboard input if we're not grabbing // Pass through keyboard input if we're not grabbing
@@ -529,21 +494,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
int eventX = (int)event.getX(actionIndex); int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex); int eventY = (int)event.getY(actionIndex);
// Special handling for 3 finger gesture
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
event.getPointerCount() == 3) {
// Three fingers down
threeFingerDownTime = SystemClock.uptimeMillis();
// Cancel the first and second touches to avoid
// erroneous events
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
}
return true;
}
TouchContext context = getTouchContext(actionIndex); TouchContext context = getTouchContext(actionIndex);
if (context == null) { if (context == null) {
return false; return false;
@@ -557,16 +507,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
break; break;
case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
if (event.getPointerCount() == 1) {
// All fingers up
if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
// This is a 3 finger tap to bring up the keyboard
showKeyboard();
return true;
}
}
context.touchUpEvent(eventX, eventY); context.touchUpEvent(eventX, eventY);
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) { if (actionIndex == 0 && event.getPointerCount() > 1) {
// The original secondary touch now becomes primary // The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1)); context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
} }
@@ -574,27 +516,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_MOVE:
// ACTION_MOVE is special because it always has actionIndex == 0 // ACTION_MOVE is special because it always has actionIndex == 0
// We'll call the move handlers for all indexes manually // 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) { for (TouchContext aTouchContextMap : touchContextMap) {
if (aTouchContextMap.getActionIndex() < event.getPointerCount()) aTouchContextMap.touchMoveEvent(eventX, eventY);
{
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())
{
aTouchContextMap.touchMoveEvent(
(int)event.getX(aTouchContextMap.getActionIndex()),
(int)event.getY(aTouchContextMap.getActionIndex()));
}
} }
break; break;
default: default:
@@ -639,12 +562,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
// First process the history
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
// Now process the current values
updateMousePosition((int)event.getX(), (int)event.getY()); updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState(); lastButtonState = event.getButtonState();
@@ -712,7 +629,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override @Override
public void stageStarting(Stage stage) { public void stageStarting(Stage stage) {
if (spinner != null) { if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName()); spinner.setMessage("Starting "+stage.getName());
} }
} }
@@ -743,8 +660,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (!displayedFailureDialog) { if (!displayedFailureDialog) {
displayedFailureDialog = true; displayedFailureDialog = true;
stopConnection(); stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
} }
} }
@@ -755,8 +671,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
e.printStackTrace(); e.printStackTrace();
stopConnection(); stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title), Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
getResources().getString(R.string.conn_terminated_msg), true);
} }
} }
+169 -192
View File
@@ -4,27 +4,19 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider; import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener; import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager; import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState; import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender; import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually; import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings; import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
@@ -41,20 +33,21 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton; import android.widget.ArrayAdapter;
import android.widget.RelativeLayout; import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity implements AdapterFragmentCallbacks { public class PcView extends Activity {
private RelativeLayout noPcFoundLayout; private Button settingsButton, addComputerButton;
private PcGridAdapter pcGridAdapter; private ListView pcList;
private ArrayAdapter<ComputerObject> pcListAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling; private boolean freezeUpdates, runningPolling;
private final ServiceConnection serviceConnection = new ServiceConnection() { private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((ComputerManagerService.ComputerManagerBinder)binder);
@@ -95,21 +88,43 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int UNPAIR_ID = 3; private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4; private final static int WOL_ID = 4;
private final static int DELETE_ID = 5; private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private void initializeViews() { private void initializeViews() {
setContentView(R.layout.activity_pc_view); setContentView(R.layout.activity_pc_view);
UiHelper.notifyNewRootView(this);
// Set default preferences if we've never been run // Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view // Setup the list view
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton); settingsButton = (Button)findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc); addComputerButton = (Button)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);
settingsButton.setOnClickListener(new OnClickListener() { settingsButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -124,38 +139,24 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
}); });
getFragmentManager().beginTransaction() if (pcListAdapter.isEmpty()) {
.replace(R.id.pcFragmentContainer, new AdapterFragment()) addListPlaceholder();
.commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE);
} }
else { else {
noPcFoundLayout.setVisibility(View.INVISIBLE); pcListAdapter.notifyDataSetChanged();
} }
pcGridAdapter.notifyDataSetChanged();
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// Bind to the computer manager service // Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this, pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
PreferenceConfiguration.readPreferences(this).listMode, pcListAdapter.setNotifyOnChange(false);
PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews(); initializeViews();
} }
@@ -174,7 +175,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
PcView.this.runOnUiThread(new Runnable() { PcView.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
updateComputer(details); updateListView(details);
} }
}); });
} }
@@ -240,32 +241,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); ComputerObject computer = pcListAdapter.getItem(info.position);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) { if (computer == null || computer.details == null) {
startComputerUpdates(); startComputerUpdates();
return; return;
} }
// Inflate the context menu // Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol)); menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
} }
else if (computer.details.pairState != PairState.PAIRED) { else if (computer.details.pairState != PairState.PAIRED) {
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc)); menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc)); if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
}
} }
else { else {
if (computer.details.runningGameId != 0) { menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
} }
} }
@@ -276,25 +271,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private void doPair(final ComputerDetails computer) { private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
return; return;
} }
if (computer.runningGameId != 0) { if (computer.runningGameId != 0) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, "Computer is currently in a game. " +
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
return; return;
} }
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
boolean success = false;
try { try {
// Stop updates and wait while pairing // Stop updates and wait while pairing
stopComputerUpdates(true); stopComputerUpdates(true);
@@ -306,38 +302,29 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp; addr = computer.remoteIp;
} }
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr, httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(), PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this)); PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
// Don't display any toast, but open the app list message = "Already paired";
message = null;
success = true;
} }
else { else {
final String pinStr = PairingManager.generatePinString(); final String pinStr = PairingManager.generatePinString();
// Spin the dialog off in a thread because it blocks // Spin the dialog off in a thread because it blocks
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title), Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(pinStr); PairingManager.PairState pairState = httpConn.pair(pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) { if (pairState == PairingManager.PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin); message = "Incorrect PIN";
} }
else if (pairState == PairingManager.PairState.FAILED) { else if (pairState == PairingManager.PairState.FAILED) {
message = getResources().getString(R.string.pair_fail); message = "Pairing failed";
} }
else if (pairState == PairingManager.PairState.PAIRED) { else if (pairState == PairingManager.PairState.PAIRED) {
// Just navigate to the app view without displaying a toast message = "Paired successfully";
message = null;
success = true;
} }
else { else {
// Should be no other values // Should be no other values
@@ -345,9 +332,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
} }
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = "Failed to resolve host";
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
message = e.getMessage(); message = e.getMessage();
@@ -356,19 +344,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Dialog.closeDialogs(); Dialog.closeDialogs();
final String toastMessage = message; final String toastMessage = message;
final boolean toastSuccess = success;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (toastMessage != null) {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
} }
if (toastSuccess) {
// Open the app list after a successful pairing attemp
doAppList(computer);
}
}
}); });
// Start polling again // Start polling again
@@ -379,25 +359,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private void doWakeOnLan(final ComputerDetails computer) { private void doWakeOnLan(final ComputerDetails computer) {
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) { if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
return; return;
} }
if (computer.macAddress == null) { if (computer.macAddress == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
String message; String message;
try { try {
WakeOnLanSender.sendWolPacket(computer); WakeOnLanSender.sendWolPacket(computer);
message = getResources().getString(R.string.wol_waking_msg); 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.";
} catch (IOException e) { } catch (IOException e) {
message = getResources().getString(R.string.wol_fail); message = "Failed to send Wake-On-LAN packets";
} }
final String toastMessage = message; final String toastMessage = message;
@@ -413,15 +394,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private void doUnpair(final ComputerDetails computer) { private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
return; return;
} }
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return; return;
} }
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -435,10 +417,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) { else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp; addr = computer.remoteIp;
} }
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr, httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(), managerBinder.getUniqueId(),
@@ -447,19 +425,20 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair(); httpConn.unpair();
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) { if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
message = getResources().getString(R.string.unpair_success); message = "Unpaired successfully";
} }
else { else {
message = getResources().getString(R.string.unpair_fail); message = "Failed to unpair";
} }
} }
else { else {
message = getResources().getString(R.string.unpair_error); message = "Device was not paired";
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = "Failed to resolve host";
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) { } catch (Exception e) {
message = e.getMessage(); message = e.getMessage();
} }
@@ -477,25 +456,36 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private void doAppList(ComputerDetails computer) { private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) { if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show(); Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
return; return;
} }
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return; return;
} }
Intent i = new Intent(this, AppView.class); Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, false);
}
else {
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, true);
}
startActivity(i); startActivity(i);
} }
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position); ComputerObject computer = pcListAdapter.getItem(info.position);
switch (item.getItemId()) { switch (item.getItemId())
{
case PAIR_ID: case PAIR_ID:
doPair(computer.details); doPair(computer.details);
return true; return true;
@@ -510,74 +500,90 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
case DELETE_ID: case DELETE_ID:
if (managerBinder == null) { if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show(); Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return true; return true;
} }
managerBinder.removeComputer(computer.details.name); managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details); removeListView(computer.details);
return true; return true;
case APP_LIST_ID: case APP_LIST_ID:
doAppList(computer.details); doAppList(computer.details);
return true; return true;
case RESUME_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
return true;
case QUIT_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
}
}, null);
return true;
default: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
} }
private void removeComputer(ComputerDetails details) { private static String generateString(ComputerDetails details) {
for (int i = 0; i < pcGridAdapter.getCount(); i++) { StringBuilder str = new StringBuilder();
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); str.append(details.name).append(" - ");
if (details.state == ComputerDetails.State.ONLINE) {
if (details.equals(computer.details)) { str.append("Online ");
pcGridAdapter.removeComputer(computer); if (details.reachability == ComputerDetails.Reachability.LOCAL) {
pcGridAdapter.notifyDataSetChanged(); str.append("(Local) - ");
}
if (pcGridAdapter.getCount() == 0) { else {
// Show the "Discovery in progress" view str.append("(Remote) - ");
noPcFoundLayout.setVisibility(View.VISIBLE); }
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);
if (details.equals(computer.details)) {
pcListAdapter.remove(computer);
break; break;
} }
} }
if (pcListAdapter.getCount() == 0) {
// Add the placeholder if we're down to 0 computers
addListPlaceholder();
}
} }
private void updateComputer(ComputerDetails details) { private void updateListView(ComputerDetails details) {
String computerString = generateString(details);
ComputerObject existingEntry = null; ComputerObject existingEntry = null;
boolean placeholderPresent = false;
for (int i = 0; i < pcGridAdapter.getCount(); i++) { for (int i = 0; i < pcListAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); ComputerObject computer = pcListAdapter.getItem(i);
// If there's a placeholder, there's nothing else
if (computer.details == null) {
placeholderPresent = true;
break;
}
// Check if this is the same computer // Check if this is the same computer
if (details.uuid.equals(computer.details.uuid)) { if (details.equals(computer.details)) {
existingEntry = computer; existingEntry = computer;
break; break;
} }
@@ -585,64 +591,35 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (existingEntry != null) { if (existingEntry != null) {
// Replace the information in the existing entry // Replace the information in the existing entry
existingEntry.text = computerString;
existingEntry.details = details; existingEntry.details = details;
} }
else { else {
// Add a new entry // If the placeholder is the only object, remove it
pcGridAdapter.addComputer(new ComputerObject(details)); if (placeholderPresent) {
pcListAdapter.remove(pcListAdapter.getItem(0));
}
// Remove the "Discovery in progress" view // Add a new entry
noPcFoundLayout.setVisibility(View.INVISIBLE); pcListAdapter.add(new ComputerObject(computerString, details));
} }
// Notify the view that the data has changed // Notify the view that the data has changed
pcGridAdapter.notifyDataSetChanged(); pcListAdapter.notifyDataSetChanged();
}
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(pcGridAdapter);
listView.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(listView);
} }
public class ComputerObject { public class ComputerObject {
public String text;
public ComputerDetails details; public ComputerDetails details;
public ComputerObject(ComputerDetails details) { public ComputerObject(String text, ComputerDetails details) {
if (details == null) { this.text = text;
throw new IllegalArgumentException("details must not be null");
}
this.details = details; this.details = details;
} }
@Override @Override
public String toString() { public String toString() {
return details.name; return text;
} }
} }
} }
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
private static final int FRAME_SIZE = 960; public static final int FRAME_SIZE = 960;
private AudioTrack track; private AudioTrack track;
@@ -31,31 +31,6 @@ public class AndroidAudioRenderer implements AudioRenderer {
return false; return false;
} }
// We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = 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, bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig, channelConfig,
AudioFormat.ENCODING_PCM_16BIT), AudioFormat.ENCODING_PCM_16BIT),
@@ -64,17 +39,15 @@ public class AndroidAudioRenderer implements AudioRenderer {
// Round to next frame // Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE); bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
LimeLog.info("Audio track buffer size: "+bufferSize);
track = new AudioTrack(AudioManager.STREAM_MUSIC, track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate, sampleRate,
channelConfig, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_16BIT,
bufferSize, bufferSize,
AudioTrack.MODE_STREAM); AudioTrack.MODE_STREAM);
track.play(); track.play();
}
LimeLog.info("Audio track buffer size: "+bufferSize);
return true; return true;
} }
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider { public class AndroidCryptoProvider implements LimelightCryptoProvider {
private final File certFile; private File certFile;
private final File keyFile; private File keyFile;
private X509Certificate cert; private X509Certificate cert;
private RSAPrivateKey key; private RSAPrivateKey key;
File diff suppressed because it is too large. Load diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/** /**
* GFE's prefix for every key code * GFE's prefix for every key code
*/ */
private static final short KEY_PREFIX = (short) 0x80; public static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48; public static final int VK_0 = 48;
public static final int VK_9 = 57; public static final int VK_9 = 57;
@@ -9,27 +9,17 @@ public class TouchContext {
private int originalTouchX = 0; private int originalTouchX = 0;
private int originalTouchY = 0; private int originalTouchY = 0;
private long originalTouchTime = 0; private long originalTouchTime = 0;
private boolean cancelled;
private final NvConnection conn; private NvConnection conn;
private final int actionIndex; private int actionIndex;
private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10; private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250; private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor) public TouchContext(NvConnection conn, int actionIndex)
{ {
this.conn = conn; this.conn = conn;
this.actionIndex = actionIndex; this.actionIndex = actionIndex;
this.xFactor = xFactor;
this.yFactor = yFactor;
}
public int getActionIndex()
{
return actionIndex;
} }
private boolean isTap() private boolean isTap()
@@ -58,17 +48,12 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX; originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY; originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis(); originalTouchTime = System.currentTimeMillis();
cancelled = false;
return true; return true;
} }
public void touchUpEvent(int eventX, int eventY) public void touchUpEvent(int eventX, int eventY)
{ {
if (cancelled) {
return;
}
if (isTap()) if (isTap())
{ {
byte buttonIndex = getMouseButtonIndex(); byte buttonIndex = getMouseButtonIndex();
@@ -93,28 +78,16 @@ public class TouchContext {
{ {
// We only send moves for the primary touch point // We only send moves for the primary touch point
if (actionIndex == 0) { if (actionIndex == 0) {
int deltaX = eventX - lastTouchX; conn.sendMouseMove((short)(eventX - lastTouchX),
int deltaY = eventY - lastTouchY; (short)(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; lastTouchX = eventX;
lastTouchY = eventY; lastTouchY = eventY;
}
return true; return true;
} }
public void cancelTouch() { return false;
cancelled = true;
}
public boolean isCancelled() {
return cancelled;
} }
} }
@@ -29,9 +29,9 @@ public class EvdevEvent {
/* Keys */ /* Keys */
public static final short KEY_Q = 16; public static final short KEY_Q = 16;
public final short type; public short type;
public final short code; public short code;
public final int value; public int value;
public EvdevEvent(short type, short code, int value) { public EvdevEvent(short type, short code, int value) {
this.type = type; this.type = type;
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
public class EvdevHandler { public class EvdevHandler {
private final String absolutePath; private String absolutePath;
private final EvdevListener listener; private EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private final Thread handlerThread = new Thread() { private Thread handlerThread = new Thread() {
@Override @Override
public void run() { public void run() {
// All the finally blocks here make this code look like a mess // All the finally blocks here make this code look like a mess
@@ -1,7 +1,7 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
import android.os.Build; import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
@@ -12,32 +12,30 @@ public class EvdevReader {
System.loadLibrary("evdev_reader"); System.loadLibrary("evdev_reader");
} }
public static void patchSeLinuxPolicies() {
//
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
// We should probably do something clever with a separate daemon and talk via a localhost
// socket. We don't return the SELinux policies back to default after we're done which I feel
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
// normally be able to do with SELinux enforcing on Lollipop.
//
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
// 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance();
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
}
}
// Requires root to chmod /dev/input/eventX // Requires root to chmod /dev/input/eventX
public static void setPermissions(String[] files, int octalPermissions) { public static boolean setPermissions(String[] files, int octalPermissions) {
EvdevShell shell = EvdevShell.getInstance(); ProcessBuilder builder = new ProcessBuilder("su");
try {
Process p = builder.start();
OutputStream stdin = p.getOutputStream();
for (String file : files) { for (String file : files) {
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file)); stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
} }
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
} }
// Returns the fd to be passed to other function or -1 on error // Returns the fd to be passed to other function or -1 on error
@@ -1,116 +0,0 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
import java.util.UUID;
public class EvdevShell {
private OutputStream stdin;
private InputStream stdout;
private Process shell;
private final String uuidString = UUID.randomUUID().toString();
private static final EvdevShell globalShell = new EvdevShell();
public static EvdevShell getInstance() {
return globalShell;
}
public void startShell() {
ProcessBuilder builder = new ProcessBuilder("su");
try {
// Redirect stderr to stdout
builder.redirectErrorStream(true);
shell = builder.start();
stdin = shell.getOutputStream();
stdout = shell.getInputStream();
} catch (IOException e) {
// This is unexpected
e.printStackTrace();
// Kill the shell if it spawned
if (stdin != null) {
try {
stdin.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdin = null;
}
}
if (stdout != null) {
try {
stdout.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdout = null;
}
}
if (shell != null) {
shell.destroy();
shell = null;
}
}
}
public void runCommand(String command) {
if (shell == null) {
// Shell never started
return;
}
try {
// Write the command followed by an echo with our UUID
stdin.write((command+'\n').getBytes("UTF-8"));
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
stdin.flush();
// This is the only command in flight so we can use a scanner
// without worrying about it eating too many characters
Scanner scanner = new Scanner(stdout);
while (scanner.hasNext()) {
if (scanner.next().contains(uuidString)) {
// Our command ran
return;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopShell() throws InterruptedException {
boolean exitWritten = false;
if (shell == null) {
// Shell never started
return;
}
try {
stdin.write("exit\n".getBytes("UTF-8"));
exitWritten = true;
} catch (IOException e) {
// We'll destroy the process without
// waiting for it to terminate since
// we don't know whether our exit command made it
e.printStackTrace();
}
if (exitWritten) {
try {
shell.waitFor();
} finally {
shell.destroy();
}
}
else {
shell.destroy();
}
}
}
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
private static final short[] EVDEV_KEY_CODES = { public static final short EVDEV_KEY_CODES[] = {
0, //KeyEvent.VK_RESERVED 0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,
@@ -20,8 +20,6 @@ public class EvdevWatcher {
private EvdevListener listener; private EvdevListener listener;
private Thread startThread; private Thread startThread;
private static boolean patchedSeLinuxPolicies = false;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override @Override
public void onEvent(int event, String fileName) { public void onEvent(int event, String fileName) {
@@ -119,15 +117,6 @@ public class EvdevWatcher {
startThread = new Thread() { startThread = new Thread() {
@Override @Override
public void run() { public void run() {
// Initialize the root shell
EvdevShell.getInstance().startShell();
// Patch SELinux policies (if needed)
if (!patchedSeLinuxPolicies) {
EvdevReader.patchSeLinuxPolicies();
patchedSeLinuxPolicies = true;
}
// List all files and allow us access // List all files and allow us access
File[] files = rundownWithPermissionsChange(0666); File[] files = rundownWithPermissionsChange(0666);
@@ -150,12 +139,7 @@ public class EvdevWatcher {
} }
// Giveup eventX permissions // Giveup eventX permissions
rundownWithPermissionsChange(0660); rundownWithPermissionsChange(066);
// Kill the root shell
try {
EvdevShell.getInstance().stopShell();
} catch (InterruptedException e) {}
} }
}; };
startThread.start(); startThread.start();
@@ -5,6 +5,7 @@ import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.os.Build; import android.os.Build;
@@ -18,16 +19,16 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder; import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock") @SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread, decoderThread; private Thread rendererThread;
private int targetFps; private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024; private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer; private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value // Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 5; private static final int WAIT_CEILING_MS = 8;
private static final int LOW_PERF = 1; private static final int LOW_PERF = 1;
private static final int MED_PERF = 2; private static final int MED_PERF = 2;
@@ -36,7 +37,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private int totalFrames; private int totalFrames;
private long totalTimeMs; private long totalTimeMs;
private final int cpuCount = Runtime.getRuntime().availableProcessors(); private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused") @SuppressWarnings("unused")
private int findOptimalPerformanceLevel() { private int findOptimalPerformanceLevel() {
@@ -107,7 +108,9 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
case LOW_PERF: case LOW_PERF:
// Disable the loop filter for performance reasons // Disable the loop filter for performance reasons
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING; avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Use plenty of threads to try to utilize the CPU as best we can // Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1; threadCount = cpuCount - 1;
@@ -115,7 +118,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
default: default:
case MED_PERF: case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING; avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency // Only use 2 threads to minimize frame processing latency
threadCount = 2; threadCount = 2;
@@ -141,9 +145,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
throw new IllegalStateException("AVC decoder initialization failure: "+err); throw new IllegalStateException("AVC decoder initialization failure: "+err);
} }
if (!AvcDecoder.setRenderTarget(sh.getSurface())) { AvcDecoder.setRenderTarget(sh.getSurface());
return false;
}
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
@@ -154,40 +156,23 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
@Override @Override
public boolean start(final VideoDepacketizer depacketizer) { 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() { rendererThread = new Thread() {
@Override @Override
public void run() { public void run() {
long nextFrameTime = MediaCodecHelper.getMonotonicMillis(); long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted()) while (!isInterrupted())
{ {
long diff = nextFrameTime - MediaCodecHelper.getMonotonicMillis(); du = depacketizer.pollNextDecodeUnit();
if (du != null) {
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) { if (diff > WAIT_CEILING_MS) {
try { LockSupport.parkNanos(1);
Thread.sleep(diff - WAIT_CEILING_MS);
} catch (InterruptedException e) {
return;
}
continue; continue;
} }
@@ -203,20 +188,16 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
} }
private long computePresentationTimeMs(int frameRate) { private long computePresentationTimeMs(int frameRate) {
return MediaCodecHelper.getMonotonicMillis() + (1000 / frameRate); return System.currentTimeMillis() + (1000 / frameRate);
} }
@Override @Override
public void stop() { public void stop() {
rendererThread.interrupt(); rendererThread.interrupt();
decoderThread.interrupt();
try { try {
rendererThread.join(); rendererThread.join();
} catch (InterruptedException e) { } } catch (InterruptedException e) { }
try {
decoderThread.join();
} catch (InterruptedException e) { }
} }
@Override @Override
@@ -231,8 +212,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear(); decoderBuffer.clear();
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead(); for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
bbd != null; bbd = bbd.nextDescriptor) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length); decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
} }
@@ -242,8 +222,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()]; data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0; int offset = 0;
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead(); for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
bbd != null; bbd = bbd.nextDescriptor) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length); System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length; offset += bbd.length;
} }
@@ -251,11 +230,11 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0); boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) { if (success) {
long timeAfterDecode = MediaCodecHelper.getMonotonicMillis(); long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers) // Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) { if (delta >= 0 && delta < 300) {
totalTimeMs += delta; totalTimeMs += delta;
totalFrames++; totalFrames++;
} }
@@ -281,9 +260,4 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
} }
return (int)(totalTimeMs / totalFrames); return (int)(totalTimeMs / totalFrames);
} }
@Override
public String getDecoderName() {
return "CPU decoding";
}
} }
@@ -1,12 +1,11 @@
package com.limelight.binding.video; package com.limelight.binding.video;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer; import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer { public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
private EnhancedDecoderRenderer decoderRenderer; private VideoDecoderRenderer decoderRenderer;
@Override @Override
public void release() { public void release() {
@@ -56,11 +55,6 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
return decoderRenderer.getCapabilities(); return decoderRenderer.getCapabilities();
} }
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
decoderRenderer.directSubmitDecodeUnit(du);
}
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
@@ -80,14 +74,4 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
return 0; return 0;
} }
} }
@Override
public String getDecoderName() {
if (decoderRenderer != null) {
return decoderRenderer.getDecoderName();
}
else {
return null;
}
}
} }
@@ -1,7 +0,0 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract String getDecoderName();
}
@@ -13,6 +13,7 @@ import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer; import com.limelight.nvstream.av.video.VideoDepacketizer;
import android.annotation.TargetApi;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
@@ -21,21 +22,17 @@ import android.media.MediaCodec.CodecException;
import android.os.Build; import android.os.Build;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { @SuppressWarnings("unused")
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
// Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers;
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder; private MediaCodec videoDecoder;
private Thread rendererThread; private Thread rendererThread;
private final boolean needsSpsBitstreamFixup, isExynos4; private boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer; private VideoDepacketizer depacketizer;
private final boolean adaptivePlayback, directSubmit; private boolean adaptivePlayback;
private int initialWidth, initialHeight; private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
private long lastTimestampUs; private long lastTimestampUs;
private long totalTimeMs; private long totalTimeMs;
private long decoderTimeMs; private long decoderTimeMs;
@@ -46,6 +43,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private int numPpsIn; private int numPpsIn;
private int numIframeIn; private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() { public MediaCodecDecoderRenderer() {
//dumpDecoders(); //dumpDecoders();
@@ -55,33 +55,24 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
if (decoder == null) { if (decoder == null) {
// This case is handled later in setup() // This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit = false;
return; return;
} }
decoderName = decoder.getName(); decoderName = decoder.getName();
// Set decoder-specific attributes // Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder); adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder); needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsSpsBitstreamFixup) { if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup"); LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
} }
if (needsBaselineSpsHack) { isExynos4 = MediaCodecHelper.isExynos4Device();
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
}
if (isExynos4) { if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4"); LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
} }
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
}
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width; this.initialWidth = width;
@@ -109,6 +100,52 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
} }
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) {
try {
// FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true);
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) {
// What do we do here?
e.printStackTrace();
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace();
}
else {
LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace();
}
}
});
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0); videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
@@ -117,7 +154,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return true; return true;
} }
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) { @TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) { if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e; CodecException codecExc = (CodecException) e;
@@ -132,100 +170,23 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
} }
// Only throw if this happens at the beginning of a stream
if (totalFrames < 60) {
if (buf != null || codecFlags != 0) { if (buf != null || codecFlags != 0) {
throw new RendererException(this, e, buf, codecFlags); throw new RendererException(dr, e, buf, codecFlags);
} }
else { else {
throw new RendererException(this, e); throw new RendererException(dr, e);
}
} }
} }
private void startDirectSubmitRendererThread() private void startRendererThread()
{
rendererThread = new Thread() {
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
rendererThread.start();
}
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
int index;
long startTime, queueTime;
startTime = MediaCodecHelper.getMonotonicMillis();
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
if (index < 0) {
return index;
}
queueTime = MediaCodecHelper.getMonotonicMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
}
return index;
}
private void startLegacyRendererThread()
{ {
rendererThread = new Thread() { rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@Override @Override
public void run() { public void run() {
BufferInfo info = new BufferInfo(); BufferInfo info = new BufferInfo();
DecodeUnit du = null; DecodeUnit du = null;
int inputIndex = -1; int inputIndex = -1;
long lastDuDequeueTime = 0;
while (!isInterrupted()) while (!isInterrupted())
{ {
// In order to get as much data to the decoder as early as possible, // In order to get as much data to the decoder as early as possible,
@@ -233,26 +194,22 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (inputIndex == -1 && du == null) { if (inputIndex == -1 && du == null) {
try { try {
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
inputIndex = dequeueInputBuffer(false, false); inputIndex = videoDecoder.dequeueInputBuffer(0);
du = depacketizer.pollNextDecodeUnit(); du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
notifyDuReceived(du);
}
// Stop if we can't get a DU or input buffer // Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) { if (du == null || inputIndex == -1) {
break; break;
} }
submitDecodeUnit(du, inputIndex); submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
du = null; du = null;
inputIndex = -1; inputIndex = -1;
} }
} catch (Exception e) { } catch (Exception e) {
inputIndex = -1; inputIndex = -1;
handleDecoderException(e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
@@ -267,31 +224,22 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// If we've got a DU waiting to be given to the decoder, // If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise // wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately. // just see if we can get one immediately.
inputIndex = dequeueInputBuffer(du != null, false); inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
} catch (Exception e) { } catch (Exception e) {
inputIndex = -1; inputIndex = -1;
handleDecoderException(e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
// Grab a decode unit if we don't have one already // Grab a decode unit if we don't have one already
if (du == null) { if (du == null) {
du = depacketizer.pollNextDecodeUnit(); du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
notifyDuReceived(du);
}
} }
// If we've got both a decode unit and an input buffer, we'll // If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one. // submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) { if (du != null && inputIndex >= 0) {
long submissionTime = MediaCodecHelper.getMonotonicMillis(); submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
if (submissionTime - lastDuDequeueTime >= 20) {
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
}
submitDecodeUnit(du, inputIndex);
// DU and input buffer have both been consumed // DU and input buffer have both been consumed
du = null; du = null;
@@ -317,8 +265,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoDecoder.releaseOutputBuffer(lastIndex, true); videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers) // Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000); long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta >= 0 && delta < 1000) { if (delta > 5 && delta < 300) {
decoderTimeMs += delta; decoderTimeMs += delta;
totalTimeMs += delta; totalTimeMs += delta;
} }
@@ -331,6 +279,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
LockSupport.parkNanos(1); LockSupport.parkNanos(1);
} }
break; break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed"); LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat()); LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
@@ -340,7 +291,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
} }
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
} }
@@ -358,17 +309,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Start the decoder // Start the decoder
videoDecoder.start(); videoDecoder.start();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // On devices pre-Lollipop, we'll use a rendering thread
legacyInputBuffers = videoDecoder.getInputBuffers(); if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread();
} }
if (directSubmit) {
startDirectSubmitRendererThread();
}
else {
startLegacyRendererThread();
}
return true; return true;
} }
@@ -401,11 +346,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
for (i = 0; i < 25; i++) { for (i = 0; i < 25; i++) {
try { try {
videoDecoder.queueInputBuffer(inputBufferIndex, videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length, 0, length,
timestampUs, codecFlags); timestampUs, codecFlags);
break; break;
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(e, null, codecFlags); handleDecoderException(this, e, null, codecFlags);
lastException = e; lastException = e;
} }
} }
@@ -415,27 +360,16 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
} }
// Using the new getInputBuffer() API on Lollipop allows
// the framework to do some performance optimizations for us
private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) {
ByteBuffer buf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
}
else {
buf = legacyInputBuffers[inputBufferIndex];
// Clear old input data pre-Lollipop
buf.clear();
}
return buf;
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) { private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long timestampUs = System.nanoTime() / 1000; long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
long timestampUs = currentTime * 1000;
if (timestampUs <= lastTimestampUs) { if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp // We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing // so bump it up by one before queuing
@@ -443,7 +377,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
lastTimestampUs = timestampUs; lastTimestampUs = timestampUs;
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex); // Clear old input data
buf.clear();
int codecFlags = 0; int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags(); int decodeUnitFlags = decodeUnit.getFlags();
@@ -455,10 +390,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
numIframeIn++; numIframeIn++;
} }
boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferHead(); ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
if (header.data[header.offset+4] == 0x67) { if (header.data[header.offset+4] == 0x67) {
numSpsIn++; numSpsIn++;
@@ -469,23 +402,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
SeqParameterSet sps = SeqParameterSet.read(spsBuf); SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// Some decoders rely on H264 level to decide how many buffers are needed
// Since we only need one frame buffered, we'll set the level as low as we can
// for known resolution combinations
if (initialWidth == 1280 && initialHeight == 720) {
// Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32");
sps.level_idc = 32;
}
else if (initialWidth == 1920 && initialHeight == 1080) {
// Max 4 buffered frames at 1920x1080x64
LimeLog.info("Patching level_idc to 42");
sps.level_idc = 42;
}
else {
// Leave the profile alone (currently 5.0)
}
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// also requires this fixup. // also requires this fixup.
// //
@@ -495,56 +411,19 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
LimeLog.info("Patching num_ref_frames in SPS"); LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1; sps.num_ref_frames = 1;
// GFE 2.5.11 changed the SPS to add additional extensions
// Some devices don't like these so we remove them here.
sps.vuiParams.video_signal_type_present_flag = false;
sps.vuiParams.colour_description_present_flag = false;
sps.vuiParams.colour_primaries = 2;
sps.vuiParams.transfer_characteristics = 2;
sps.vuiParams.matrix_coefficients = 2;
sps.vuiParams.chroma_loc_info_present_flag = false;
sps.vuiParams.chroma_sample_loc_type_bottom_field = 0;
sps.vuiParams.chroma_sample_loc_type_top_field = 0;
if (needsSpsBitstreamFixup || isExynos4) { if (needsSpsBitstreamFixup || isExynos4) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag // The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra. // or max_dec_frame_buffering which increases decoding latency on Tegra.
// GFE 2.5.11 started sending bitstream restrictions
if (sps.vuiParams.bitstreamRestriction == null) {
LimeLog.info("Adding bitstream restrictions"); LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16; sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
} sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
else {
LimeLog.info("Patching bitstream restrictions");
}
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
// These values are the defaults for the fields, but they are more aggressive
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
// conservative values by GFE 2.5.11. We'll let those values stand.
}
else {
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
// will continue to not receive them now
sps.vuiParams.bitstreamRestriction = null;
}
// 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 // Write the annex B header
@@ -561,20 +440,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return; return;
} else if (header.data[header.offset+4] == 0x68) { } else if (header.data[header.offset+4] == 0x68) {
numPpsIn++; 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;
}
} }
} }
// Copy data from our buffer list into the input buffer // Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead(); for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
desc != null; desc = desc.nextDescriptor) { {
buf.put(desc.data, desc.offset, desc.length); buf.put(desc.data, desc.offset, desc.length);
} }
@@ -583,48 +454,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
timestampUs, codecFlags); timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit); depacketizer.freeDecodeUnit(decodeUnit);
if (needsSpsReplay) {
replaySps();
}
}
private void replaySps() {
int inputIndex = dequeueInputBuffer(true, true);
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
// 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.nanoTime() / 1000,
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
LimeLog.info("SPS replay complete");
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
int caps = 0; return adaptivePlayback ?
caps |= adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
caps |= directSubmit ?
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return caps;
} }
@Override @Override
@@ -643,45 +478,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return (int)(totalTimeMs / totalFrames); return (int)(totalTimeMs / totalFrames);
} }
@Override
public String getDecoderName() {
return decoderName;
}
private void notifyDuReceived(DecodeUnit du) {
long currentTime = MediaCodecHelper.getMonotonicMillis();
long delta = currentTime-du.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-du.getReceiveTimestamp();
totalFrames++;
}
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
int inputIndex;
notifyDuReceived(du);
for (;;) {
try {
inputIndex = dequeueInputBuffer(true, true);
break;
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
if (inputIndex >= 0) {
submitDecodeUnit(du, inputIndex);
}
}
public class RendererException extends RuntimeException { public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L; private static final long serialVersionUID = 8985937536997012406L;
private final Exception originalException; private Exception originalException;
private final MediaCodecDecoderRenderer renderer; private MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer; private ByteBuffer currentBuffer;
private int currentCodecFlags; private int currentCodecFlags;
@@ -20,27 +20,11 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
private static final List<String> preferredDecoders; public static final List<String> preferredDecoders;
private static final List<String> blacklistedDecoderPrefixes; public static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes; public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes; public static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
static {
directSubmitPrefixes = new LinkedList<String>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
}
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -59,10 +43,6 @@ public class MediaCodecHelper {
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>(); whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia"); whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
@@ -84,16 +64,8 @@ public class MediaCodecHelper {
return false; return false;
} }
public static long getMonotonicMillis() {
return System.nanoTime() / 1000000L;
}
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) { 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)) { if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)"); LimeLog.info("Adaptive playback supported (whitelist)");
return true; return true;
@@ -112,23 +84,15 @@ public class MediaCodecHelper {
} catch (Exception e) { } catch (Exception e) {
// Tolerate buggy codecs // Tolerate buggy codecs
} }
}*/ }
return false; return false;
} }
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) { public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
} }
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@SuppressLint("NewApi") @SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() { private static LinkedList<MediaCodecInfo> getMediaCodecList() {
@@ -169,7 +133,7 @@ public class MediaCodecHelper {
return str; return str;
} }
private static MediaCodecInfo findPreferredDecoder() { public static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions, // This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order // because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order // rather than MediaCodecList's order
@@ -240,7 +204,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly // since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility // and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows") @SuppressWarnings("RedundantThrows")
private static MediaCodecInfo findKnownSafeDecoder() throws Exception { public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -100,7 +100,6 @@ public class ComputerDatabaseManager {
// This signifies we don't have dynamic state (like pair state) // 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 a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
@@ -108,7 +107,6 @@ public class ComputerDatabaseManager {
continue; continue;
} }
computerList.add(details); computerList.add(details);
} }
@@ -154,9 +152,6 @@ public class ComputerDatabaseManager {
c.close(); c.close();
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, delete the database entry // If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) { details.macAddress == null) {
@@ -1,25 +1,22 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.util.HashMap;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; 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.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService; import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -28,25 +25,19 @@ import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service { public class ComputerManagerService extends Service {
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000; private static final int POLLING_PERIOD_MS = 5000;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int OFFLINE_POLL_TRIES = 3;
private final ComputerManagerBinder binder = new ComputerManagerBinder(); private ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private final AtomicInteger dbRefCount = new AtomicInteger(0); private AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private HashMap<ComputerDetails, Thread> pollingThreads;
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0); private AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
@@ -69,7 +60,10 @@ public class ComputerManagerService extends Service {
}; };
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException { private boolean runPoll(ComputerDetails details)
{
boolean newPc = (details.name == null);
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -77,23 +71,12 @@ public class ComputerManagerService extends Service {
activePolls.incrementAndGet(); activePolls.incrementAndGet();
// Poll the machine // Poll the machine
try { if (!doPollMachine(details)) {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
// Return without calling the listener
releaseLocalDatabaseReference();
return false;
}
details.state = ComputerDetails.State.OFFLINE; details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE; details.reachability = ComputerDetails.Reachability.OFFLINE;
} }
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
throw e;
} finally {
activePolls.decrementAndGet(); activePolls.decrementAndGet();
}
// If it's online, update our persistent state // If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) { if (details.state == ComputerDetails.State.ONLINE) {
@@ -103,7 +86,7 @@ public class ComputerManagerService extends Service {
if (dbManager.getComputerByName(details.name) == null) { if (dbManager.getComputerByName(details.name) == null) {
// It's gone // It's gone
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
return false; return true;
} }
} }
@@ -123,21 +106,22 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
while (!isInterrupted()) {
ComputerDetails originalDetails = new ComputerDetails();
originalDetails.update(details);
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
try {
// Check if this poll has modified the details // Check if this poll has modified the details
if (!runPoll(details, false, offlineCount)) { if (runPoll(details) && !originalDetails.equals(details)) {
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")"); // Replace our thread entry with the new one
offlineCount++; synchronized (pollingThreads) {
pollingThreads.remove(originalDetails);
pollingThreads.put(details, this);
} }
else {
offlineCount = 0;
} }
// Wait until the next polling interval // Wait until the next polling interval
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1)); try {
Thread.sleep(POLLING_PERIOD_MS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
} }
@@ -150,24 +134,26 @@ public class ComputerManagerService extends Service {
public class ComputerManagerBinder extends Binder { public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) { public void startPolling(ComputerManagerListener listener) {
// Polling is active
pollingActive = true;
// Set the listener // Set the listener
ComputerManagerService.this.listener = listener; ComputerManagerService.this.listener = listener;
// Start mDNS autodiscovery too // Start mDNS autodiscovery too
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
synchronized (pollingTuples) { // Start polling known machines
for (PollingTuple tuple : pollingTuples) { if (!getLocalDatabaseReference()) {
// This polling thread might already be there return;
if (tuple.thread == null) { }
// Report this computer initially List<ComputerDetails> computerList = dbManager.getAllComputers();
listener.notifyComputerUpdated(tuple.computer); releaseLocalDatabaseReference();
tuple.thread = createPollingThread(tuple.computer); synchronized (pollingThreads) {
tuple.thread.start(); for (ComputerDetails computer : computerList) {
// This polling thread might already be there
if (!pollingThreads.containsKey(computer)) {
Thread t = createPollingThread(computer);
pollingThreads.put(computer, t);
t.start();
} }
} }
} }
@@ -197,6 +183,10 @@ public class ComputerManagerService extends Service {
return ComputerManagerService.this.addComputerBlocking(addr); return ComputerManagerService.this.addComputerBlocking(addr);
} }
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) { public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name); ComputerManagerService.this.removeComputer(name);
} }
@@ -206,25 +196,9 @@ public class ComputerManagerService extends Service {
ComputerManagerService.this.onUnbind(null); ComputerManagerService.this.onUnbind(null);
} }
public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer);
}
public String getUniqueId() { public String getUniqueId() {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
return null;
}
} }
@Override @Override
@@ -233,15 +207,11 @@ public class ComputerManagerService extends Service {
discoveryBinder.stopDiscovery(); discoveryBinder.stopDiscovery();
// Stop polling // Stop polling
pollingActive = false; synchronized (pollingThreads) {
synchronized (pollingTuples) { for (Thread t : pollingThreads.values()) {
for (PollingTuple tuple : pollingTuples) { t.interrupt();
if (tuple.thread != null) {
// Interrupt and remove the thread
tuple.thread.interrupt();
tuple.thread = null;
}
} }
pollingThreads.clear();
} }
// Remove the listener // Remove the listener
@@ -255,7 +225,7 @@ public class ComputerManagerService extends Service {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine // Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress()); addComputer(computer.getAddress());
} }
@Override @Override
@@ -271,32 +241,19 @@ public class ComputerManagerService extends Service {
}; };
} }
private void addTuple(ComputerDetails details) { public void addComputer(InetAddress addr) {
synchronized (pollingTuples) { // Setup a placeholder
for (PollingTuple tuple : pollingTuples) { ComputerDetails fakeDetails = new ComputerDetails();
// Check if this is the same computer fakeDetails.localIp = addr;
if (tuple.computer.uuid.equals(details.uuid)) { fakeDetails.remoteIp = addr;
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
tuple.computer.localIp = details.localIp;
tuple.computer.remoteIp = details.remoteIp;
// Start a polling thread if polling is active // Spawn a thread for this computer
if (pollingActive && tuple.thread == null) { synchronized (pollingThreads) {
tuple.thread = createPollingThread(details); // This polling thread might already be there
tuple.thread.start(); if (!pollingThreads.containsKey(fakeDetails)) {
} Thread t = createPollingThread(fakeDetails);
pollingThreads.put(fakeDetails, t);
// Found an entry so we're done t.start();
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();
} }
} }
} }
@@ -308,23 +265,10 @@ public class ComputerManagerService extends Service {
fakeDetails.remoteIp = addr; fakeDetails.remoteIp = addr;
// Block while we try to fill the details // Block while we try to fill the details
try { runPoll(fakeDetails);
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
// If the machine is reachable, it was successful // If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) { return fakeDetails.state == ComputerDetails.State.ONLINE;
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine
addTuple(fakeDetails);
return true;
}
else {
return false;
}
} }
public void removeComputer(String name) { public void removeComputer(String name) {
@@ -335,20 +279,6 @@ public class ComputerManagerService extends Service {
// Remove it from the database // Remove it from the database
dbManager.deleteComputer(name); 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(); releaseLocalDatabaseReference();
} }
@@ -367,127 +297,34 @@ public class ComputerManagerService extends Service {
} }
} }
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) { private ComputerDetails tryPollIp(InetAddress ipAddr) {
try { try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails(); return 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) { } catch (Exception e) {
return null; return null;
} }
} }
// Just try to establish a TCP connection to speculatively detect a running private boolean pollComputer(ComputerDetails details, boolean localFirst) {
// GFE server
private boolean fastPollIp(InetAddress addr) {
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
return false;
}
}
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
Thread t = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
synchronized (info) {
info[0] = true; // Done
info[1] = pollRes; // Polling result
info.notify();
}
}
};
t.setName("Fast Poll - "+addr.getHostAddress());
t.start();
}
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
startFastPollThread(local, localInfo);
startFastPollThread(remote, remoteInfo);
// Check local first
synchronized (localInfo) {
while (!localInfo[0]) {
localInfo.wait(500);
}
if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL;
}
}
// Now remote
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
}
if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE;
}
}
return ComputerDetails.Reachability.OFFLINE;
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails; ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
reachability = ComputerDetails.Reachability.REMOTE;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
}
// If no connection could be established to either IP address, there's nothing we can do
if (reachability == ComputerDetails.Reachability.OFFLINE) {
return false;
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) { if (localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details.localIp);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteIp); polledDetails = tryPollIp(details.remoteIp);
} }
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback // Failed, so let's try the fallback
if (!localFirst) { if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp); polledDetails = tryPollIp(details.localIp);
} }
else { else {
polledDetails = tryPollIp(details, details.remoteIp); polledDetails = tryPollIp(details.remoteIp);
} }
// The fallback poll worked // The fallback poll worked
@@ -506,19 +343,13 @@ public class ComputerManagerService extends Service {
return false; return false;
} }
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable // If we got here, it's reachable
details.update(polledDetails); details.update(polledDetails);
return true;
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
} }
return true; private boolean doPollMachine(ComputerDetails details) {
return pollComputer(details, true);
} }
@Override @Override
@@ -527,24 +358,14 @@ public class ComputerManagerService extends Service {
bindService(new Intent(this, DiscoveryService.class), bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE); discoveryServiceConnection, Service.BIND_AUTO_CREATE);
pollingThreads = new HashMap<ComputerDetails, Thread>();
// Lookup or generate this device's UID // Lookup or generate this device's UID
idManager = new IdentityManager(this); idManager = new IdentityManager(this);
// Initialize the DB // Initialize the DB
dbManager = new ComputerDatabaseManager(this); dbManager = new ComputerDatabaseManager(this);
dbRefCount.set(1); 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 @Override
@@ -564,125 +385,4 @@ public class ComputerManagerService extends Service {
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
public class ApplistPoller {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
}
public void pollNow() {
synchronized (pollEvent) {
pollEvent.notify();
}
}
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
}
return thread != null && !thread.isInterrupted();
}
public void start() {
thread = new Thread() {
@Override
public void run() {
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) {
listener.notifyComputerUpdated(computer);
}
continue;
}
// Can't poll if there's no UUID yet
if (computer.uuid == null) {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
try {
// Query the app list from the server
String appList = http.getAppListRaw();
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException ignored) {}
}
// Update the computer
computer.rawAppList = appList;
// Notify that the app list has been updated
// and ensure that the thread is still active
if (listener != null && thread != null) {
listener.notifyComputerUpdated(computer);
}
}
else {
LimeLog.warning("Empty app list received from "+computer.uuid);
}
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
} while (waitPollingDelay());
}
};
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
// Don't join here because we might be blocked on network I/O
thread = null;
}
}
}
}
class PollingTuple {
public Thread thread;
public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
this.thread = thread;
}
} }
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
}); });
} }
private final DiscoveryBinder binder = new DiscoveryBinder(); private DiscoveryBinder binder = new DiscoveryBinder();
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
@@ -1,115 +0,0 @@
package com.limelight.grid;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.grid.assets.CachedAppAssetLoader;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Comparator;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader;
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
}
public void cancelQueuedOperations() {
loader.cancelForegroundLoads();
loader.cancelBackgroundLoads();
loader.freeCacheMemory();
}
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
}
});
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app);
sortList();
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
}
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView);
return true;
}
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.app.getIsRunning()) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.play);
return true;
}
// No overlay
return false;
}
}
@@ -1,82 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.R;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int defaultImageRes;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<T>();
protected final 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 (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
return convertView;
}
}
@@ -1,75 +0,0 @@
package com.limelight.grid;
import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
}
public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
}
});
}
public boolean removeComputer(PcView.ComputerObject computer) {
return itemList.remove(computer);
}
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.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;
}
}
@@ -1,337 +0,0 @@
package com.limelight.grid.assets;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.widget.ImageView;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer;
private final double scalingDivider;
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = placeholderBitmap;
}
public void cancelBackgroundLoads() {
Runnable r;
while ((r = cacheExecutor.getQueue().poll()) != null) {
cacheExecutor.remove(r);
}
}
public void cancelForegroundLoads() {
Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r);
}
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
return null;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
}
else {
// Otherwise it's a background load and we return nothing
return null;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
return null;
}
}
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
}
// Cache the bitmap
if (bmp != null) {
memoryLoader.populateCache(tuple, bmp);
}
return bmp;
}
@Override
protected void onProgressUpdate(Void... nothing) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the view
imageView.setAlpha(1.0f);
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<LoaderTask> loaderTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
}
public LoaderTask getLoaderTask() {
return loaderTaskReference.get();
}
}
private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getLoaderTask();
}
return null;
}
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
final LoaderTask loaderTask = getLoaderTask(imageView);
// Check if any task was pending for this image view
if (loaderTask != null && !loaderTask.isCancelled()) {
final LoaderTuple taskTuple = loaderTask.tuple;
// Cancel the task if it's not already loading the same data
if (taskTuple == null || !taskTuple.equals(tuple)) {
loaderTask.cancel(true);
} else {
// It's already loading what we want
return false;
}
}
// Allow the load to proceed
return true;
}
public void queueCacheLoad(NvApp app) {
final LoaderTuple tuple = new LoaderTuple(computer, app);
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
// It's in memory which means it must also be on disk
return;
}
// Queue a fetch in the cache executor
cacheExecutor.execute(new Runnable() {
@Override
public void run() {
// Check if the image is cached on disk
if (diskLoader.checkCacheExists(tuple)) {
return;
}
// Try to load the asset from the network and cache result on disk
doNetworkAssetLoad(tuple, null);
}
});
}
public void populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return;
}
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LoaderTuple)) {
return false;
}
LoaderTuple other = (LoaderTuple) o;
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
}
@@ -1,67 +0,0 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (FileNotFoundException ignored) {
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
}
}
}
@@ -1,37 +0,0 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
}
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
memoryCache.evictAll();
}
}
@@ -1,49 +0,0 @@
package com.limelight.grid.assets;
import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
private final String uniqueId;
public NetworkAssetLoader(Context context, String uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
}
else {
LimeLog.info("Network asset load failed: " + tuple);
}
return in;
}
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
}
@@ -2,36 +2,32 @@ package com.limelight.preferences;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.View;
import android.view.inputmethod.EditorInfo; import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class AddComputerManually extends Activity { public class AddComputerManually extends Activity {
private Button addPcButton;
private TextView hostText; private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread; private Thread addThread;
private final ServiceConnection serviceConnection = new ServiceConnection() { private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) { public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread(); startAddThread();
@@ -46,26 +42,20 @@ public class AddComputerManually extends Activity {
private void doAddPc(String host) { private void doAddPc(String host) {
String msg; String msg;
boolean finish = false; boolean finish = false;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false);
try { try {
InetAddress addr = InetAddress.getByName(host); InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){ if (!managerBinder.addComputerBlocking(addr)){
msg = getResources().getString(R.string.addpc_fail); msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
} }
else { else {
msg = getResources().getString(R.string.addpc_success); msg = "Successfully added computer";
finish = true; finish = true;
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host); msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
} }
dialog.dismiss();
final boolean toastFinish = finish; final boolean toastFinish = finish;
final String toastMsg = msg; final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() { AddComputerManually.this.runOnUiThread(new Runnable() {
@@ -119,7 +109,6 @@ public class AddComputerManually extends Activity {
super.onStop(); super.onStop();
Dialog.closeDialogs(); Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this);
} }
@Override @Override
@@ -136,46 +125,26 @@ public class AddComputerManually extends Activity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_add_computer_manually); setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this); this.addPcButton = (Button) findViewById(R.id.addPc);
this.hostText = (TextView) findViewById(R.id.hostTextView); this.hostText = (TextView) findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostText.getText().toString().trim());
}
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
return false;
}
return false;
}
});
// Bind to the ComputerManager service // Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this, bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
addPcButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
computersToAdd.add(hostText.getText().toString());
}
});
} }
} }
@@ -2,8 +2,6 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
@@ -14,16 +12,11 @@ public class PreferenceConfiguration {
private static final String SOPS_PREF_STRING = "checkbox_enable_sops"; 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 DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10; private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 20; private static final int BITRATE_DEFAULT_1080_60 = 30;
private static final String DEFAULT_RES_FPS = "720p60"; private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto"; private static final String DEFAULT_DECODER = "auto";
@@ -32,10 +25,6 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_SOPS = true; private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false; private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -44,10 +33,7 @@ public class PreferenceConfiguration {
public int width, height, fps; public int width, height, fps;
public int bitrate; public int bitrate;
public int decoder; public int decoder;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -68,26 +54,6 @@ public class PreferenceConfiguration {
} }
} }
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null) {
// TVs shouldn't use small mode by default
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
return false;
}
// API 21 uses LEANBACK instead of TELEVISION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return false;
}
}
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) { public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -164,18 +130,11 @@ public class PreferenceConfiguration {
config.decoder = getDecoderValue(context); config.decoder = getDecoderValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences // Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }
@@ -19,15 +19,11 @@ public class SeekBarPreference extends DialogPreference
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android"; private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private SeekBar seekBar; private SeekBar seekBar;
private TextView valueText; private TextView splashText, valueText;
private final Context context; private Context context;
private final String dialogMessage; private String dialogMessage, suffix;
private final String suffix; private int defaultValue, maxValue, currentValue;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) { public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -51,10 +47,9 @@ public class SeekBarPreference extends DialogPreference
suffix = context.getString(suffixId); suffix = context.getString(suffixId);
} }
// Get default, min, and max seekbar values // Get default and max seekbar values
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context)); defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100); maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
} }
@Override @Override
@@ -65,7 +60,7 @@ public class SeekBarPreference extends DialogPreference
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6); layout.setPadding(6, 6, 6, 6);
TextView splashText = new TextView(context); splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10); splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) { if (dialogMessage != null) {
splashText.setText(dialogMessage); splashText.setText(dialogMessage);
@@ -76,7 +71,7 @@ public class SeekBarPreference extends DialogPreference
valueText.setGravity(Gravity.CENTER_HORIZONTAL); valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32); valueText.setTextSize(32);
params = new LinearLayout.LayoutParams( params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.FILL_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params); layout.addView(valueText, params);
@@ -84,13 +79,8 @@ public class SeekBarPreference extends DialogPreference
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) { public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
if (value < minValue) {
seekBar.setProgress(minValue);
return;
}
String t = String.valueOf(value); String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix)); valueText.setText(suffix == null ? t : t.concat(" " + suffix));
} }
@Override @Override
@@ -100,7 +90,7 @@ public class SeekBarPreference extends DialogPreference
public void onStopTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {}
}); });
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) { if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue); currentValue = getPersistedInt(defaultValue);
@@ -131,6 +121,13 @@ public class SeekBarPreference extends DialogPreference
} }
} }
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) { public void setProgress(int progress) {
this.currentValue = progress; this.currentValue = progress;
if (seekBar != null) { if (seekBar != null) {
@@ -152,10 +149,10 @@ public class SeekBarPreference extends DialogPreference
if (shouldPersist()) { if (shouldPersist()) {
currentValue = seekBar.getProgress(); currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress()); persistInt(seekBar.getProgress());
callChangeListener(seekBar.getProgress()); callChangeListener(Integer.valueOf(seekBar.getProgress()));
} }
getDialog().dismiss(); ((AlertDialog) getDialog()).dismiss();
} }
}); });
} }
@@ -1,21 +0,0 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
public class SmallIconCheckboxPreference extends CheckBoxPreference {
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return PreferenceConfiguration.getDefaultSmallMode(getContext());
}
}
@@ -1,48 +1,23 @@
package com.limelight.preferences; package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_stream_settings); setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace( getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment() R.id.stream_settings, new SettingsFragment()
).commit(); ).commit();
UiHelper.notifyNewRootView(this);
}
@Override
public void onBackPressed() {
finish();
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
} }
public static class SettingsFragment extends PreferenceFragment { public static class SettingsFragment extends PreferenceFragment {
@@ -1,35 +0,0 @@
package com.limelight.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import com.limelight.R;
public class AdapterFragment extends Fragment {
private AdapterFragmentCallbacks callbacks;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callbacks = (AdapterFragmentCallbacks) activity;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
}
}
@@ -1,8 +0,0 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -1,5 +0,0 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -1,74 +0,0 @@
package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
}
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
}
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
}
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
}
public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
char[] buf = new char[256];
int bytesRead;
while ((bytesRead = r.read(buf)) != -1) {
sb.append(buf, 0, bytesRead);
}
try {
in.close();
} catch (IOException ignored) {}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,16 +7,15 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private final String title; private String title, message;
private final String message; private Activity activity;
private final Activity activity; private boolean endAfterDismiss;
private final boolean endAfterDismiss;
private AlertDialog alert; private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>(); private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss) public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
@@ -1,90 +0,0 @@
package com.limelight.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
parent.startActivity(intent);
}
public static void doQuit(final Activity parent,
final InetAddress address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
}
else {
message = e.getMessage();
}
} catch (UnknownHostException e) {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
if (onComplete != null) {
onComplete.run();
}
}
final String toastMessage = message;
parent.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
}
@@ -9,15 +9,14 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener { public class SpinnerDialog implements Runnable,OnCancelListener {
private final String title; private String title, message;
private final String message; private Activity activity;
private final Activity activity;
private ProgressDialog progress; private ProgressDialog progress;
private final boolean finish; private boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>(); private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
private SpinnerDialog(Activity activity, String title, String message, boolean finish) public SpinnerDialog(Activity activity, String title, String message, boolean finish)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
@@ -1,62 +0,0 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.view.View;
import com.limelight.R;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void notifyNewRootView(Activity activity)
{
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}
+11 -11
View File
@@ -69,6 +69,9 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
return -1; return -1;
} }
// Show frames even before a reference frame
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
if (perf_lvl & DISABLE_LOOP_FILTER) { if (perf_lvl & DISABLE_LOOP_FILTER) {
// Skip the loop filter for performance reasons // Skip the loop filter for performance reasons
decoder_ctx->skip_loop_filter = AVDISCARD_ALL; decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
@@ -367,20 +370,17 @@ int nv_avc_decode(unsigned char* indata, int inlen) {
// Only copy the picture at the end of decoding the packet // Only copy the picture at the end of decoding the packet
if (got_pic) { 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); pthread_mutex_lock(&mutex);
old_frame = yuv_frame;
yuv_frame = new_frame;
pthread_mutex_unlock(&mutex);
// Free the old frame outside of the mutex // Only clone this frame if the last frame was taken.
if (old_frame != NULL) { // This saves on extra copies for frames that don't get
av_frame_free(&old_frame); // rendered.
if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
} }
pthread_mutex_unlock(&mutex);
} }
return err < 0 ? err : 0; return err < 0 ? err : 0;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@@ -8,67 +8,48 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" > tools:context=".PcView" >
<RelativeLayout <ListView
android:id="@+id/no_pc_found_layout" android:id="@+id/pcListView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="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>
<FrameLayout
android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc"
android:layout_toRightOf="@+id/settingsButton"
android:layout_toEndOf="@+id/settingsButton"/>
<ImageButton
android:id="@+id/settingsButton"
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_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="@drawable/settings"
style="?android:attr/borderlessButtonStyle"/>
<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_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_below="@+id/settingsButton"
android:src="@drawable/add_computer" android:background="@drawable/list_view_unselected"
style="?android:attr/borderlessButtonStyle"/> android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
</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
android:id="@+id/settingsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/pcListView"
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" />
</RelativeLayout> </RelativeLayout>
@@ -8,65 +8,46 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" > tools:context=".PcView" >
<RelativeLayout <ListView
android:id="@+id/no_pc_found_layout" android:id="@+id/pcListView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="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>
<FrameLayout
android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
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_alignParentBottom="true"
android:layout_alignParentTop="true"/>
<ImageButton
android:id="@+id/settingsButton"
android:layout_width="70dp"
android:layout_height="65dp"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:src="@drawable/settings"
style="?android:attr/borderlessButtonStyle"/>
<ImageButton
android:id="@+id/manuallyAddPc"
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:layout_alignParentRight="true"
android:src="@drawable/add_computer" android:layout_below="@+id/discoveryText"
style="?android:attr/borderlessButtonStyle"/> android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
</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
android:id="@+id/settingsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:text="@string/button_stream_settings" />
<Button
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" />
</RelativeLayout> </RelativeLayout>
@@ -5,28 +5,16 @@
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="10dp"
tools:context=".AddComputerManually" > tools:context=".Connection" >
<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 <EditText
android:id="@+id/hostTextView" android:id="@+id/hostTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText"
android:layout_marginTop="25dp"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentTop="true"
android:ems="10" android:ems="10"
android:singleLine="true" android:singleLine="true"
android:inputType="textNoSuggestions" android:inputType="textNoSuggestions"
@@ -35,4 +23,12 @@
<requestFocus /> <requestFocus />
</EditText> </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> </RelativeLayout>
+11 -10
View File
@@ -8,27 +8,28 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" > tools:context=".AppView" >
<FrameLayout <ListView
android:id="@+id/appFragmentContainer" android:id="@+id/pcListView"
android:layout_width="fill_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText"/> android:layout_below="@+id/appListText"
android:fastScrollEnabled="true"
android:longClickable="false"
android:background="@drawable/list_view_unselected"
android:stackFromBottom="false">
</ListView>
<TextView <TextView
android:id="@+id/appListText" android:id="@+id/appListText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="10dp" android:paddingBottom="10dp" />
android:textSize="28sp"/>
</RelativeLayout> </RelativeLayout>
+3 -2
View File
@@ -1,7 +1,8 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000"
tools:context=".Game" > tools:context=".Game" >
<SurfaceView <SurfaceView
@@ -10,4 +11,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" /> android:layout_gravity="center" />
</merge> </FrameLayout>
-41
View File
@@ -1,41 +0,0 @@
<?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>
@@ -1,41 +0,0 @@
<?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="10dp">
<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="100dp"
android:layout_height="117dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</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="14sp" >
</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:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" >
</ListView>
</LinearLayout>
-37
View File
@@ -1,37 +0,0 @@
<?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>
@@ -1,37 +0,0 @@
<?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="15dp">
<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="100dp"
android:layout_height="67dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="10dp"
android:layout_marginLeft="42dp"
android:layout_marginStart="42dp"
android:layout_marginRight="13dp"
android:layout_marginEnd="13dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="100dp"
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="14sp" >
</TextView>
</RelativeLayout>
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -5,7 +5,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/grid_text" android:id="@+id/rowTextView"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textIsSelectable="false" android:textIsSelectable="false"
-15
View File
@@ -1,15 +0,0 @@
<?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>
-120
View File
@@ -1,120 +0,0 @@
<?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>
<string name="yes">Sì</string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</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>
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</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 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_checkbox_multi_controller">Supporto controller multipli</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Moonlight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</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</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ò ridurre la latenza video quando si usano impostazioni streaming basse</string>
</resources>
@@ -0,0 +1,8 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw600dp devices (e.g. 7" tablets) here.
-->
</resources>
@@ -0,0 +1,9 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
-->
<dimen name="activity_horizontal_margin">128dp</dimen>
</resources>
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- <!--
Base application theme for API 21+. This theme completely replaces Base application theme for API 21+. This theme completely replaces
AppBaseTheme from BOTH res/values/styles.xml and AppBaseTheme from BOTH res/values/styles.xml and
res/values-v21/styles.xml on API 21+ devices. res/values-v11/styles.xml on API 21+ devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme.Material"> <style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. --> <!-- API 21 theme customizations can go here. -->
+4 -15
View File
@@ -6,32 +6,21 @@
<item>1080p 30 FPS</item> <item>1080p 30 FPS</item>
<item>1080p 60 FPS</item> <item>1080p 60 FPS</item>
</string-array> </string-array>
<string-array name="resolution_values" translatable="false"> <string-array name="resolution_values">
<item>720p30</item> <item>720p30</item>
<item>720p60</item> <item>720p60</item>
<item>1080p30</item> <item>1080p30</item>
<item>1080p60</item> <item>1080p60</item>
</string-array> </string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
<item>Italiano</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
</string-array>
<string-array name="decoder_names"> <string-array name="decoder_names">
<item>Auto-select Decoder</item>
<item>Force Software Decoding</item> <item>Force Software Decoding</item>
<item>Auto-select Decoder</item>
<item>Force Hardware Decoding</item> <item>Force Hardware Decoding</item>
</string-array> </string-array>
<string-array name="decoder_values" translatable="false"> <string-array name="decoder_values">
<item>auto</item>
<item>software</item> <item>software</item>
<item>auto</item>
<item>hardware</item> <item>hardware</item>
</string-array> </string-array>
</resources> </resources>
+14
View File
@@ -0,0 +1,14 @@
<resources>
<!--
Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels.
-->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="buttonBarStyle" format="reference" />
<attr name="buttonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>
+6 -92
View File
@@ -1,87 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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 frame decoding latency:</string>
<string name="conn_client_latency_hw">hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoding 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 --> <!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string> <string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<!-- AppList activity --> <!-- PC view activity -->
<string name="title_applist">Apps on</string> <string name="title_pc_view">PC List</string>
<string name="applist_menu_resume">Resume Session</string> <string name="button_stream_settings">Streaming Settings</string>
<string name="applist_menu_quit">Quit Session</string> <string name="button_add_pc_manually">Add PC Manually</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>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string> <string name="button_add_pc">Manually Add PC</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 --> <!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string> <string name="category_basic_settings">Basic Settings</string>
@@ -94,27 +23,12 @@
<string name="title_checkbox_disable_warnings">Disable warning messages</string> <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="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Moonlight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="category_host_settings">Host Settings</string> <string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game 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> <string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
<string name="title_checkbox_host_audio">Play audio on PC</string> <string name="title_checkbox_host_audio">Play audio on PC</string>
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string> <string name="summary_checkbox_host_audio">Play audio from the computer and this device. Requires GFE 2.1.2+</string>
<string name="category_advanced_settings">Advanced Settings</string> <string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</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> </resources>
+15 -10
View File
@@ -4,14 +4,12 @@
<!-- <!--
Base application theme, dependent on API level. This theme is replaced Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.␍ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.␍
--> -->
<style name="AppBaseTheme" parent="android:Theme"> <style name="AppBaseTheme" parent="android:Theme">
<!-- <!--
Theme customizations available in newer API levels can go in Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.␍ backward-compatibility can go here.␍
--> -->
</style> </style>
@@ -22,14 +20,21 @@
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
</style> </style>
<!-- Stream activity theme --> <style name="FullscreenTheme" parent="android:Theme.NoTitleBar">
<style name="StreamTheme" parent="AppBaseTheme"> <item name="android:windowContentOverlay">@null</item>
<!-- All customizations that are NOT specific to a particular API-level can go here. --> <item name="android:windowBackground">@null</item>
<item name="android:windowActionBar">false</item> <item name="buttonBarStyle">@style/ButtonBar</item>
<item name="android:windowNoTitle">true</item> <item name="buttonBarButtonStyle">@style/ButtonBarButton</item>
<!-- Transparent streaming background to avoid extra overdraw -->
<item name="android:windowBackground">@android:color/transparent</item>
</style> </style>
<style name="ButtonBar">
<item name="android:paddingLeft">2dp</item>
<item name="android:paddingTop">5dp</item>
<item name="android:paddingRight">2dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="android:background">@android:drawable/bottom_bar</item>
</style>
<style name="ButtonBarButton" />
</resources> </resources>
-32
View File
@@ -26,19 +26,6 @@
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </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"/-->
<CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings"> <PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_enable_sops" android:key="checkbox_enable_sops"
@@ -51,31 +38,12 @@
android:summary="@string/summary_checkbox_host_audio" android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
android:entries="@array/language_names"
android:entryValues="@array/language_values"
android:summary="@string/summary_language_list"
android:defaultValue="default" />
<com.limelight.preferences.SmallIconCheckboxPreference
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"> <PreferenceCategory android:title="@string/category_advanced_settings">
<ListPreference <ListPreference
android:key="list_decoders" android:key="list_decoders"
android:title="@string/title_decoder_list" android:title="@string/title_decoder_list"
android:entries="@array/decoder_names" android:entries="@array/decoder_names"
android:entryValues="@array/decoder_values" android:entryValues="@array/decoder_values"
android:summary="@string/summary_decoder_list"
android:defaultValue="auto" /> android:defaultValue="auto" />
</PreferenceCategory> </PreferenceCategory>
+3 -2
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight" >
<!-- Non-root application name --> <!-- Non-root application name -->
<application android:label="Moonlight" /> <application android:label="Limelight" />
</manifest> </manifest>
+3 -2
View File
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight.root" >
<!-- Root permissions --> <!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" /> <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<!-- Root application name --> <!-- Root application name -->
<application android:label="Moonlight (Root)" /> <application android:label="Limelight (Root)" />
</manifest> </manifest>
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:1.3.0' classpath 'com.android.tools.build:gradle:0.13.2'
} }
} }
+2 -8
View File
@@ -4,12 +4,12 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- Affected decoders: TI OMAP4, Allwinner A20 - 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. 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, Broadcom VideoCore IV - Affected decoders: NVIDIA Tegra 3 and 4
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't 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 - Affected decoders: TI OMAP4
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame 4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only - 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 5. Some decoders will hang if max_dec_frame_buffering is not present
@@ -17,9 +17,3 @@ This file serves to document some of the decoder errata when using MediaCodec ha
6. Some decoders will hang if max_dec_frame_buffering IS present 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
+2 -2
View File
@@ -1,6 +1,6 @@
#Sun Dec 07 22:52:07 PST 2014 #Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip
@@ -1,14 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4"> <module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle"> <facet type="java-gradle" name="Java-Gradle">
<configuration> <configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" /> <option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" /> <excludeFolder url="file://$MODULE_DIR$/.gradle" />
@@ -17,3 +16,4 @@
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>