Compare commits

...

104 Commits

Author SHA1 Message Date
Cameron Gutman 5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman 3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman 5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman 2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman 10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman 55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman 265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman 4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman 47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman 6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman 2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman 07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05:00
Ansa89 39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman 4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman 256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman 5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman 7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman 9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
Cameron Gutman 3a0c1db168 Merge pull request #54 from Ansa89/italian-translation
Italian translation: update
2015-02-01 00:44:17 -05:00
Cameron Gutman bd21692323 Properly center text on the app view 2015-02-01 00:43:57 -05:00
Cameron Gutman 5ae245bdca Trim spaces from the IP address 2015-02-01 00:39:47 -05:00
Cameron Gutman d3052cd97d Set small icon by default on phones 2015-02-01 00:33:43 -05:00
Cameron Gutman 336f85a31c Fix loading bugs with uncached images 2015-01-31 22:14:12 -05:00
Cameron Gutman b01f7c796e Fix duplicated fragments 2015-01-31 17:19:54 -05:00
Cameron Gutman 56f438fe47 Fix some crashes and caching issues 2015-01-31 17:01:46 -05:00
Cameron Gutman baa5199b83 Load cached images in the background to avoid stalling the UI thread 2015-01-31 16:59:45 -05:00
Cameron Gutman 23ca62b304 Fix dp constant 2015-01-31 14:00:47 -05:00
Cameron Gutman 2c3511195c Use small mode by default on things smaller than 7 inch tablets 2015-01-31 13:21:21 -05:00
Ansa89 d31ef481f3 Italian translation: update 2015-01-31 10:03:37 +01:00
Cameron Gutman a490da5e5c Fix some caching bugs 2015-01-31 00:13:51 -05:00
Cameron Gutman 72d3576257 Fix a crash and a hang in the new computer manager code 2015-01-30 19:33:42 -05:00
Cameron Gutman ebd93a55a0 Fix small icon mode 2015-01-30 19:17:00 -05:00
Cameron Gutman 4d01e1afe6 Stub icon scaling and allow background updating of the applist 2015-01-30 18:49:01 -05:00
Cameron Gutman 9ff1386751 Add a quit confirmation dialog 2015-01-27 15:31:01 -05:00
Cameron Gutman 5fca35f0b1 Sort app list alphabetically 2015-01-26 20:58:33 -05:00
Cameron Gutman d23c763441 Remove unused imports 2015-01-26 20:50:14 -05:00
Cameron Gutman fa058c4783 Merge pull request #53 from Ansa89/italian-translation
Italian translation: update
2015-01-26 20:47:00 -05:00
Ansa89 e0ddd5f045 Italian translation: update 2015-01-26 10:53:30 +01:00
Cameron Gutman b7443451a4 Fix release build failure for beta 2015-01-25 23:38:33 -05:00
Cameron Gutman e90e4a22c4 Increment version 2015-01-25 23:35:24 -05:00
Cameron Gutman 3a53172145 Apply list mode preference immediately 2015-01-25 23:28:13 -05:00
Cameron Gutman 1dfcb7bc29 Fix root input device capture on the Nexus 9 2015-01-25 23:16:32 -05:00
Cameron Gutman 897bb76858 Forgot this file 2015-01-25 22:58:17 -05:00
Cameron Gutman bcc67269ab Add gestures to bring up the software keyboard - Long press start or tap with 3 fingers 2015-01-25 22:55:12 -05:00
Cameron Gutman 4d24c654b9 Remove the old fragment when adding the new one 2015-01-25 22:11:38 -05:00
Cameron Gutman cba44b091b Add common with GFE 2.1.x backwards compatibility 2015-01-25 21:35:03 -05:00
Cameron Gutman f2d8f8a41b Update preference text 2015-01-25 21:04:27 -05:00
Cameron Gutman 4b1c7e7e3c Fix state loss crashes 2015-01-25 21:04:13 -05:00
Cameron Gutman 1cba278876 Cache box art locally 2015-01-25 21:00:34 -05:00
Cameron Gutman 766898fdf9 Add list support back for users that don't like the grid 2015-01-25 20:23:35 -05:00
Cameron Gutman 13e91d594b Fix Lint and build issues 2015-01-25 18:50:31 -05:00
Cameron Gutman ca0a0da19f Fix fusion of computers that were re-added after becoming unreachable 2015-01-25 18:41:44 -05:00
Cameron Gutman 82cabce86e Merge pull request #33 from Ansa89/language_chooser
Add language chooser
2015-01-25 16:46:10 -05:00
Cameron Gutman 51a630995a Update common for GFE 2.2.2+ support 2015-01-22 15:29:58 -05:00
Cameron Gutman 3a74f0726c Updated libs 2015-01-22 15:29:46 -05:00
Cameron Gutman efa6c7bba0 Fix build of root package 2015-01-22 15:29:16 -05:00
Michelle Bergeron b8141542f8 Add wiki link 2015-01-17 18:44:31 -08:00
Cameron Gutman 8fc9a90207 Switch back to Maven repos of ion and androidasync packages 2014-12-13 20:41:32 -08:00
Cameron Gutman 13d707d98d Use final release of Gradle 1.0 2014-12-09 01:13:13 -08:00
Cameron Gutman aae0ff6e7a Migrate the project to Android Studio 1.0 RC4 2014-12-08 00:12:37 -08:00
Cameron Gutman 69c7b5a0d5 Update version 2014-12-03 20:52:52 -08:00
Cameron Gutman d1ad3115fa Add remote to stream config 2014-12-02 00:55:46 -08:00
Cameron Gutman 770af402a4 Reduce default 1080p60 bitrate to 20 Mbps 2014-12-02 00:55:31 -08:00
Cameron Gutman 3236c0b93a Lower the level_idc of the SPS to the minimum required for streaming at a given resolution 2014-12-01 22:58:52 -08:00
Cameron Gutman 51aacc3f38 Remove extra newlines 2014-12-01 22:39:17 -08:00
Cameron Gutman 397c6f46f9 Fix a security issue which caused input devices to remain world readable after the stream is ended 2014-12-01 22:29:16 -08:00
Cameron Gutman d00f78f859 Revert square to circle analog work since it seems to be handled correctly already 2014-12-01 22:27:02 -08:00
Cameron Gutman 29fec2e0de Add initial support for rooted devices running Lollipop with SELinux set to enforcing. This should really be improved in the future since we're modifying policies for untrusted_app. 2014-12-01 22:26:35 -08:00
Cameron Gutman 88d28665ef Attempt to fix IndexOutOfBoundsException (index 0 size 0) reported by a couple users 2014-11-30 18:34:34 -06:00
Cameron Gutman de1f4da258 Apply the square to circle plane mapping before evaluating the deadzone. Cleanup some dead code. 2014-11-30 15:52:49 -06:00
Cameron Gutman 7985be57ab Translate the analog stick values of controllers with "square" analog stick planes (DS3, DS4, and others) to the circular plane that XInput programs expect 2014-11-30 15:35:20 -06:00
Cameron Gutman a835e7aaa2 Increase DS4 controller responsiveness by ignoring historical values again 2014-11-30 12:34:30 -06:00
Ansa89 22958cfbb1 Language chooser: use constants 2014-11-29 14:40:03 +01:00
Cameron Gutman c4dc5eb9e1 Update common for faster IDR recovery 2014-11-28 22:17:42 -06:00
Cameron Gutman db758f386e Comment out unused variable 2014-11-28 22:16:46 -06:00
Cameron Gutman 3fb3eefa94 Fix Nyko Playpad input issue 2014-11-28 22:16:33 -06:00
Ansa89 9340dff45d PreferenceConfiguration.java: add language preference 2014-11-28 10:26:14 +01:00
Cameron Gutman 2d6c756e70 Always consider a PC to be remote if localIP == remoteIP 2014-11-27 21:56:20 -06:00
Cameron Gutman 03e965d449 Merge pull request #34 from Ansa89/italian-translation
Italian translation: better wording
2014-11-27 20:35:57 -06:00
Cameron Gutman 34f72544d8 Increment version 2014-11-25 14:56:40 -08:00
Cameron Gutman d839ea9781 Increase deadzone on triggers to Xinput defaults and add special handling of the Nexus Player Controller and Nexus Remote 2014-11-25 14:54:36 -08:00
Cameron Gutman 2b7f13fdbb Increase max frame time to improve accuracy of latency counter 2014-11-25 13:34:00 -08:00
Cameron Gutman 7557a3a4ae Don't capture the back button on remotes 2014-11-25 11:16:47 -08:00
Cameron Gutman fcecba484f Fix a crash caught by Monkey 2014-11-25 02:05:24 -08:00
Cameron Gutman fa85a0a0bd Improve CPU decoder frame latency when rendering speed is less than decoding speed 2014-11-25 02:04:51 -08:00
Cameron Gutman dc64bfeba2 Slightly reduce max packet size in an attempt to cut packet losses 2014-11-25 01:05:55 -08:00
Cameron Gutman 871b73c48d Fix PC duplication issue when multiple machines report the same remote IP address 2014-11-24 20:10:02 -08:00
Cameron Gutman 5dcff91d27 Only grab Fire TV remotes if a gamepad isn't attached 2014-11-24 18:43:08 -08:00
Cameron Gutman 0041fc1dab Fix broken de-duplication of computers 2014-11-24 18:25:58 -08:00
Cameron Gutman 314242ab08 Update to Ion with fixes for SSLContext and self-signed certificates 2014-11-24 18:10:23 -08:00
Cameron Gutman 09e8ddfd74 Use the bitstream restrictions fixup on Broadcom VideoCore IV devices 2014-11-24 18:03:47 -08:00
Ansa89 4cea483a87 Italian translation: better wording 2014-11-24 11:53:18 +01:00
Ansa89 99aa616188 Add language chooser
Implement limelight-stream/limelight-android#32.
2014-11-24 11:47:02 +01:00
Cameron Gutman 444c4602c1 Update libraries. Seems to improve image caching behavior with Ion. 2014-11-23 23:39:20 -08:00
Cameron Gutman 5b6eac7140 Update build.gradle for re-release 2014-11-23 02:07:03 -08:00
Cameron Gutman 7cdd184197 Fix null pointer exception on ATV emulator 2014-11-23 02:06:49 -08:00
67 changed files with 6012 additions and 4752 deletions
+2
View File
@@ -8,6 +8,8 @@ whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-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/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
##Features ##Features
* Streams any of your games from your PC to your Android device * Streams any of your games from your PC to your Android device
+10 -6
View File
@@ -9,6 +9,7 @@
<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="assembleNonRootDebugTest" /> <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
@@ -24,6 +25,7 @@
</component> </component>
<component name="NewModuleRootManager" 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/test/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" />
@@ -31,6 +33,7 @@
<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/generated/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" />
@@ -43,6 +46,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/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" />
@@ -101,18 +105,18 @@
</content> </content>
<orderEntry type="jdk" jdkName="Android API 21 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="okhttp-2.1.0-RC1" level="project" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" /> <orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="androidasync-1.3.7" level="project" /> <orderEntry type="library" exported="" name="support-v4-r7" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" /> <orderEntry type="library" exported="" name="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="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="support-v4-r6" level="project" /> <orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
<orderEntry type="library" exported="" name="okio-1.0.1" level="project" /> <orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
<orderEntry type="library" exported="" name="ion-1.3.7" level="project" /> <orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
</component> </component>
</module> </module>
+14 -9
View File
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 21
buildToolsVersion "21.0.2" buildToolsVersion "21.1.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 21
versionName "3.0" versionName "3.1"
versionCode = 45 versionCode = 53
} }
productFlavors { productFlavors {
@@ -27,7 +27,7 @@ android {
buildTypes { buildTypes {
release { release {
runProguard false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
} }
@@ -62,13 +62,18 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3' compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.android', name: 'support-v4', version:'r6'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'1.3.7' compile group: 'com.google.android', name: 'support-v4', version:'r7'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1' compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.squareup.okio', name:'okio', version:'1.0.1' compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile files('libs/jmdns-fixed.jar') compile files('libs/jmdns-fixed.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.
+1 -1
View File
@@ -68,7 +68,7 @@
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.Connection" /> android:value="com.limelight.AppView" />
</activity> </activity>
<service <service
android:name=".discovery.DiscoveryService" android:name=".discovery.DiscoveryService"
+446 -232
View File
@@ -1,92 +1,482 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.xmlpull.v1.XmlPullParserException; import java.util.UUID;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter; import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.ComputerDetails;
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.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.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
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.GridView;
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 { public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter; private AppGridAdapter appGridAdapter;
private InetAddress ipAddress; private String uuidString;
private String uniqueId;
private boolean remote;
private boolean firstLoad = true;
private final static int RESUME_ID = 1; private ComputerDetails computer;
private final static int QUIT_ID = 2; private ComputerManagerService.ApplistPoller poller;
private final static int CANCEL_ID = 3; private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
public final static String ADDRESS_EXTRA = "Address"; private int consecutiveAppListFailures = 0;
public final static String UNIQUEID_EXTRA = "UniqueId"; private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote";
@Override private final static int START_OR_RESUME_ID = 1;
protected void onCreate(Bundle savedInstanceState) { private final static int QUIT_ID = 2;
super.onCreate(savedInstanceState); private final static int CANCEL_ID = 3;
setContentView(R.layout.activity_app_view); private final static int START_WTIH_QUIT = 4;
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers
if (details != computer) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// 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;
}
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
try {
lastRawApplist = details.rawAppList;
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();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA); uuidString = getIntent().getStringExtra(UUID_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
finish();
return;
}
String labelText = getResources().getString(R.string.title_applist)+" "+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);
try { // Bind to the computer manager service
ipAddress = InetAddress.getByAddress(address); bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
} catch (UnknownHostException e) { Service.BIND_AUTO_CREATE);
e.printStackTrace(); }
finish();
return;
}
// Setup the list view private void populateAppGridWithCache() {
GridView appGrid = (GridView) findViewById(R.id.appGridView);
try { try {
appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId); // Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); if (lastRawApplist != null) {
finish(); LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
}
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
}
private void loadAppsBlocking() {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return; return;
} }
appGrid.setAdapter(appGridAdapter);
appGrid.setOnItemClickListener(new OnItemClickListener() { int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
}
@Override
public void onContextMenuClosed(Menu menu) {
}
private void displayQuitConfirmationDialog(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(this);
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
.show();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case START_WTIH_QUIT:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doStart(app.app);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
return true;
case QUIT_ID:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doQuit(app.app);
}
}, null);
return true;
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
}
}
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
for (NvApp app : appList) {
boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app == null) {
continue;
}
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
}
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
}
else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
}
} catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).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 @Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos, public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) { long id) {
@@ -103,196 +493,20 @@ public class AppView extends Activity {
} }
} }
}); });
registerForContextMenu(appGrid); registerForContextMenu(listView);
} listView.requestFocus();
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
}
@Override
protected void onResume() {
super.onResume();
// Display the error message if it was the
// first load, but just kill the activity
// on subsequent errors
updateAppList(firstLoad);
firstLoad = false;
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
} }
@Override public class AppObject {
public void onContextMenuClosed(Menu menu) { public final NvApp app;
}
@Override public AppObject(NvApp app) {
public boolean onContextItemSelected(MenuItem item) { this.app = app;
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); }
AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
return true;
case QUIT_ID: @Override
doQuit(app.app); public String toString() {
return true; return app.getAppName();
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
} }
} }
private void updateAppList(final boolean displayError) {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
appGridAdapter.clear();
for (NvApp app : appList) {
appGridAdapter.addApp(new AppObject(app));
}
appGridAdapter.notifyDataSetChanged();
}
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
if (displayError) {
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
getResources().getString(R.string.applist_refresh_error_msg), true);
}
else {
// Just finish the activity immediately
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
}
});
}
}
}.start();
}
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, 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(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
}
else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
}
updateAppList(true);
} catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
public class AppObject {
public NvApp app;
public AppObject(NvApp app) {
this.app = app;
}
@Override
public String toString() {
return app.getAppName();
}
}
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
public class PlatformBinding { public class PlatformBinding {
public static String getDeviceName() { public static String getDeviceName() {
String deviceName = android.os.Build.MODEL; String deviceName = android.os.Build.MODEL;
deviceName = deviceName.replace(" ", ""); deviceName = deviceName.replace(" ", "");
return deviceName; return deviceName;
} }
public static AudioRenderer getAudioRenderer() { public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer(); return new AndroidAudioRenderer();
} }
public static LimelightCryptoProvider getCryptoProvider(Context c) { public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c); return new AndroidCryptoProvider(c);
} }
} }
@@ -9,27 +9,27 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960; private static final int FRAME_SIZE = 960;
private AudioTrack track; private AudioTrack track;
@Override @Override
public boolean streamInitialized(int channelCount, int sampleRate) { public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig; int channelConfig;
int bufferSize; int bufferSize;
switch (channelCount) switch (channelCount)
{ {
case 1: case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO; channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break; break;
case 2: case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break; break;
default: default:
LimeLog.severe("Decoder returned unhandled channel count"); LimeLog.severe("Decoder returned unhandled channel count");
return false; return false;
} }
// We're not supposed to request less than the minimum // We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can // buffer size for our buffer, but it appears that we can
@@ -73,25 +73,25 @@ public class AndroidAudioRenderer implements AudioRenderer {
track.play(); track.play();
} }
LimeLog.info("Audio track buffer size: "+bufferSize); LimeLog.info("Audio track buffer size: "+bufferSize);
return true; return true;
} }
@Override @Override
public void playDecodedAudio(byte[] audioData, int offset, int length) { public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length); track.write(audioData, offset, length);
} }
@Override @Override
public void streamClosing() { public void streamClosing() {
if (track != null) { if (track != null) {
track.release(); track.release();
} }
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return 0; return 0;
} }
} }
@@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider { public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile; private final File certFile;
private File keyFile; private final File keyFile;
private X509Certificate cert; private X509Certificate cert;
private RSAPrivateKey key; private RSAPrivateKey key;
private byte[] pemCertBytes; private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object(); private static final Object globalCryptoLock = new Object();
static { static {
// Install the Bouncy Castle provider // Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
public AndroidCryptoProvider(Context c) { public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath(); String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt"); certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key"); keyFile = new File(dataPath + File.separator + "client.key");
} }
private byte[] loadFileToBytes(File f) { private byte[] loadFileToBytes(File f) {
if (!f.exists()) { if (!f.exists()) {
return null; return null;
} }
try { try {
FileInputStream fin = new FileInputStream(f); FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()]; byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) { if (fin.read(fileData) != f.length()) {
// Failed to read // Failed to read
fileData = null; fileData = null;
} }
fin.close(); fin.close();
return fileData; return fileData;
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
} }
private boolean loadCertKeyPair() { private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile); byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile); byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed // If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) { if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one"); LimeLog.info("Missing cert or key; need to generate a new one");
return false; return false;
} }
try { try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes)); cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes; pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC"); KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) { } catch (CertificateException e) {
// May happen if the cert is corrupt // May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate"); LimeLog.warning("Corrupted certificate");
return false; return false;
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} catch (InvalidKeySpecException e) { } catch (InvalidKeySpecException e) {
// May happen if the key is corrupt // May happen if the key is corrupt
LimeLog.warning("Corrupted key"); LimeLog.warning("Corrupted key");
return false; return false;
} catch (NoSuchProviderException e) { } catch (NoSuchProviderException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
return true; return true;
} }
@SuppressLint("TrulyRandom") @SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() { private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8]; byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes); new SecureRandom().nextBytes(snBytes);
KeyPair keyPair; KeyPair keyPair;
try { try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048); keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair(); keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) { } catch (NoSuchAlgorithmException e1) {
// Should never happen // Should never happen
e1.printStackTrace(); e1.printStackTrace();
return false; return false;
} catch (NoSuchProviderException e) { } catch (NoSuchProviderException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
Date now = new Date(); Date now = new Date();
// Expires in 20 years // Expires in 20 years
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTime(now); calendar.setTime(now);
calendar.add(Calendar.YEAR, 20); calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime(); Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs(); BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client"); nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build(); X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name, X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())); SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try { try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen)); cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate(); key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) { } catch (Exception e) {
// Nothing should go wrong here // Nothing should go wrong here
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
LimeLog.info("Generated a new key pair"); LimeLog.info("Generated a new key pair");
// Save the resulting pair // Save the resulting pair
saveCertKeyPair(); saveCertKeyPair();
return true; return true;
} }
private void saveCertKeyPair() { private void saveCertKeyPair() {
try { try {
FileOutputStream certOut = new FileOutputStream(certFile); FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile); FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server) // Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter(); StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter); JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert); pemWriter.writeObject(cert);
pemWriter.close(); pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly // Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut); OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString(); String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) { for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i); char c = pemStr.charAt(i);
if (c != '\r') if (c != '\r')
certWriter.append(c); certWriter.append(c);
} }
certWriter.close(); certWriter.close();
// Write the private out in PKCS8 format // Write the private out in PKCS8 format
keyOut.write(key.getEncoded()); keyOut.write(key.getEncoded());
certOut.close(); certOut.close();
keyOut.close(); keyOut.close();
LimeLog.info("Saved generated key pair to disk"); LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) { } catch (IOException e) {
// This isn't good because it means we'll have // This isn't good because it means we'll have
// to re-pair next time // to re-pair next time
e.printStackTrace(); e.printStackTrace();
} }
} }
public X509Certificate getClientCertificate() { public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading // Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time // the certificate and key at a time
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Return a loaded cert if we have one // Return a loaded cert if we have one
if (cert != null) { if (cert != null) {
return cert; return cert;
} }
// No loaded cert yet, let's see if we have one on disk // No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) { if (loadCertKeyPair()) {
// Got one // Got one
return cert; return cert;
} }
// Try to generate a new key pair // Try to generate a new key pair
if (!generateCertKeyPair()) { if (!generateCertKeyPair()) {
// Failed // Failed
return null; return null;
} }
// Load the generated pair // Load the generated pair
loadCertKeyPair(); loadCertKeyPair();
return cert; return cert;
} }
} }
public RSAPrivateKey getClientPrivateKey() { public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading // Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time // the certificate and key at a time
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Return a loaded key if we have one // Return a loaded key if we have one
if (key != null) { if (key != null) {
return key; return key;
} }
// No loaded key yet, let's see if we have one on disk // No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) { if (loadCertKeyPair()) {
// Got one // Got one
return key; return key;
} }
// Try to generate a new key pair // Try to generate a new key pair
if (!generateCertKeyPair()) { if (!generateCertKeyPair()) {
// Failed // Failed
return null; return null;
} }
// Load the generated pair // Load the generated pair
loadCertKeyPair(); loadCertKeyPair();
return key; return key;
} }
} }
public byte[] getPemEncodedClientCertificate() { public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us // Call our helper function to do the cert loading/generation for us
getClientCertificate(); getClientCertificate();
// Return a cached value if we have it // Return a cached value if we have it
return pemCertBytes; return pemCertBytes;
} }
} }
@Override @Override
public String encodeBase64String(byte[] data) { public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP); return Base64.encodeToString(data, Base64.NO_WRAP);
} }
} }
File diff suppressed because it is too large Load Diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/** /**
* GFE's prefix for every key code * GFE's prefix for every key code
*/ */
public static final short KEY_PREFIX = (short) 0x80; private 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;
@@ -23,8 +23,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
public static final int VK_Z = 90; public static final int VK_Z = 90;
public static final int VK_ALT = 18; public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96; public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92; public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20; public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12; public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44; public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17; public static final int VK_CONTROL = 17;
@@ -4,88 +4,95 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.input.MouseButtonPacket;
public class TouchContext { public class TouchContext {
private int lastTouchX = 0; private int lastTouchX = 0;
private int lastTouchY = 0; private int lastTouchY = 0;
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 NvConnection conn; private final NvConnection conn;
private int actionIndex; private final int actionIndex;
private double xFactor, yFactor; 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, double xFactor, double yFactor)
{ {
this.conn = conn; this.conn = conn;
this.actionIndex = actionIndex; this.actionIndex = actionIndex;
this.xFactor = xFactor; this.xFactor = xFactor;
this.yFactor = yFactor; this.yFactor = yFactor;
} }
public int getActionIndex() public int getActionIndex()
{ {
return actionIndex; return actionIndex;
} }
private boolean isTap() private boolean isTap()
{ {
int xDelta = Math.abs(lastTouchX - originalTouchX); int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY); int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime; long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD && return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD && yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD; timeDelta <= TAP_TIME_THRESHOLD;
} }
private byte getMouseButtonIndex() private byte getMouseButtonIndex()
{ {
if (actionIndex == 1) { if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT; return MouseButtonPacket.BUTTON_RIGHT;
} }
else { else {
return MouseButtonPacket.BUTTON_LEFT; return MouseButtonPacket.BUTTON_LEFT;
} }
} }
public boolean touchDownEvent(int eventX, int eventY) public boolean touchDownEvent(int eventX, int eventY)
{ {
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 (isTap()) if (cancelled) {
{ return;
byte buttonIndex = getMouseButtonIndex(); }
// Lower the mouse button if (isTap())
conn.sendMouseButtonDown(buttonIndex); {
byte buttonIndex = getMouseButtonIndex();
// We need to sleep a bit here because some games // Lower the mouse button
// do input detection by polling conn.sendMouseButtonDown(buttonIndex);
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button // We need to sleep a bit here because some games
conn.sendMouseButtonUp(buttonIndex); // do input detection by polling
} try {
} Thread.sleep(100);
} catch (InterruptedException ignored) {}
public boolean touchMoveEvent(int eventX, int eventY) // Raise the mouse button
{ conn.sendMouseButtonUp(buttonIndex);
if (eventX != lastTouchX || eventY != lastTouchY) }
{ }
// We only send moves for the primary touch point
if (actionIndex == 0) { public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
int deltaX = eventX - lastTouchX; int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY; int deltaY = eventY - lastTouchY;
@@ -93,13 +100,21 @@ public class TouchContext {
deltaX = (int)Math.round((double)deltaX * xFactor); deltaX = (int)Math.round((double)deltaX * xFactor);
deltaY = (int)Math.round((double)deltaY * yFactor); deltaY = (int)Math.round((double)deltaY * yFactor);
conn.sendMouseMove((short)deltaX, (short)deltaY); conn.sendMouseMove((short)deltaX, (short)deltaY);
} }
lastTouchX = eventX; lastTouchX = eventX;
lastTouchY = eventY; lastTouchY = eventY;
} }
return true; return true;
} }
public void cancelTouch() {
cancelled = true;
}
public boolean isCancelled() {
return cancelled;
}
} }
@@ -1,41 +1,41 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public class EvdevEvent { public class EvdevEvent {
public static final int EVDEV_MIN_EVENT_SIZE = 16; public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24; public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */ /* Event types */
public static final short EV_SYN = 0x00; public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01; public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02; public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04; public static final short EV_MSC = 0x04;
/* Relative axes */ /* Relative axes */
public static final short REL_X = 0x00; public static final short REL_X = 0x00;
public static final short REL_Y = 0x01; public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08; public static final short REL_WHEEL = 0x08;
/* Buttons */ /* Buttons */
public static final short BTN_LEFT = 0x110; public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111; public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112; public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113; public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114; public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115; public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116; public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117; public static final short BTN_TASK = 0x117;
public static final short BTN_GAMEPAD = 0x130; public static final short BTN_GAMEPAD = 0x130;
/* Keys */ /* Keys */
public static final short KEY_Q = 16; public static final short KEY_Q = 16;
public short type; public final short type;
public short code; public final short code;
public int value; public final int value;
public EvdevEvent(short type, short code, int value) { public EvdevEvent(short type, short code, int value) {
this.type = type; this.type = type;
this.code = code; this.code = code;
this.value = value; this.value = value;
} }
} }
@@ -7,161 +7,161 @@ import com.limelight.LimeLog;
public class EvdevHandler { public class EvdevHandler {
private String absolutePath; private final String absolutePath;
private EvdevListener listener; private final EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private Thread handlerThread = new Thread() { private final 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
// but it's important that we get this right to avoid causing // but it's important that we get this right to avoid causing
// system-wide input problems. // system-wide input problems.
// Open the /dev/input/eventX file // Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath); fd = EvdevReader.open(absolutePath);
if (fd == -1) { if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath); LimeLog.warning("Unable to open "+absolutePath);
return; return;
} }
try { try {
// Check if it's a mouse or keyboard, but not a gamepad // Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) { EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice // We only handle keyboards and mice
return; return;
} }
// Grab it for ourselves // Grab it for ourselves
if (!EvdevReader.grab(fd)) { if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath); LimeLog.warning("Unable to grab "+absolutePath);
return; return;
} }
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
try { try {
int deltaX = 0; int deltaX = 0;
int deltaY = 0; int deltaY = 0;
byte deltaScroll = 0; byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) { while (!isInterrupted() && !shutdown) {
EvdevEvent event = EvdevReader.read(fd, buffer); EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) { if (event == null) {
return; return;
} }
switch (event.type) switch (event.type)
{ {
case EvdevEvent.EV_SYN: case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) { if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY); listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0; deltaX = deltaY = 0;
} }
if (deltaScroll != 0) { if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll); listener.mouseScroll(deltaScroll);
deltaScroll = 0; deltaScroll = 0;
} }
break; break;
case EvdevEvent.EV_REL: case EvdevEvent.EV_REL:
switch (event.code) switch (event.code)
{ {
case EvdevEvent.REL_X: case EvdevEvent.REL_X:
deltaX = event.value; deltaX = event.value;
break; break;
case EvdevEvent.REL_Y: case EvdevEvent.REL_Y:
deltaY = event.value; deltaY = event.value;
break; break;
case EvdevEvent.REL_WHEEL: case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value; deltaScroll = (byte) event.value;
break; break;
} }
break; break;
case EvdevEvent.EV_KEY: case EvdevEvent.EV_KEY:
switch (event.code) switch (event.code)
{ {
case EvdevEvent.BTN_LEFT: case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_MIDDLE: case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_RIGHT: case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT, listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_SIDE: case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA: case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD: case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK: case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK: case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons // Other unhandled mouse buttons
break; break;
default: default:
// We got some unrecognized button. This means // We got some unrecognized button. This means
// someone is trying to use the other device in this // someone is trying to use the other device in this
// "combination" input device. We'll try to handle // "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect // it via keyboard, but we're not going to disconnect
// if we can't // if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code); short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) { if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode); listener.keyboardEvent(event.value != 0, keyCode);
} }
break; break;
} }
break; break;
case EvdevEvent.EV_MSC: case EvdevEvent.EV_MSC:
break; break;
} }
} }
} finally { } finally {
// Release our grab // Release our grab
EvdevReader.ungrab(fd); EvdevReader.ungrab(fd);
} }
} finally { } finally {
// Close the file // Close the file
EvdevReader.close(fd); EvdevReader.close(fd);
} }
} }
}; };
public EvdevHandler(String absolutePath, EvdevListener listener) { public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath; this.absolutePath = absolutePath;
this.listener = listener; this.listener = listener;
} }
public void start() { public void start() {
handlerThread.start(); handlerThread.start();
} }
public void stop() { public void stop() {
// Close the fd. It doesn't matter if this races // Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from // with the handler thread. We'll close this out from
// under the thread to wake it up // under the thread to wake it up
if (fd != -1) { if (fd != -1) {
EvdevReader.close(fd); EvdevReader.close(fd);
} }
shutdown = true; shutdown = true;
handlerThread.interrupt(); handlerThread.interrupt();
try { try {
handlerThread.join(); handlerThread.join();
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
} }
public void notifyDeleted() { public void notifyDeleted() {
stop(); stop();
} }
} }
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public interface EvdevListener { public interface EvdevListener {
public static final int BUTTON_LEFT = 1; public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2; public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3; public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY); public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down); public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount); public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode); public void keyboardEvent(boolean buttonDown, short keyCode);
} }
@@ -1,103 +1,105 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
import java.io.IOException; import android.os.Build;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import com.limelight.LimeLog; import com.limelight.LimeLog;
public class EvdevReader { public class EvdevReader {
static { static {
System.loadLibrary("evdev_reader"); System.loadLibrary("evdev_reader");
} }
// Requires root to chmod /dev/input/eventX public static void patchSeLinuxPolicies() {
public static boolean setPermissions(String[] files, int octalPermissions) { //
ProcessBuilder builder = new ProcessBuilder("su"); // 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 }\"");
}
}
try { // Requires root to chmod /dev/input/eventX
Process p = builder.start(); public static void setPermissions(String[] files, int octalPermissions) {
EvdevShell shell = EvdevShell.getInstance();
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(); // Returns the fd to be passed to other function or -1 on error
p.destroy(); public static native int open(String fileName);
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false; // Prevent other apps (including Android itself) from using the device while "grabbed"
} public static native boolean grab(int fd);
public static native boolean ungrab(int fd);
// Returns the fd to be passed to other function or -1 on error // Used for checking device capabilities
public static native int open(String fileName); public static native boolean hasRelAxis(int fd, short axis);
public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key);
// Prevent other apps (including Android itself) from using the device while "grabbed" public static boolean isMouse(int fd) {
public static native boolean grab(int fd); // This is the same check that Android does in EventHub.cpp
public static native boolean ungrab(int fd); return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
// Used for checking device capabilities public static boolean isAlphaKeyboard(int fd) {
public static native boolean hasRelAxis(int fd, short axis); // This is the same check that Android does in EventHub.cpp
public static native boolean hasAbsAxis(int fd, short axis); return hasKey(fd, EvdevEvent.KEY_Q);
public static native boolean hasKey(int fd, short key); }
public static boolean isMouse(int fd) { public static boolean isGamepad(int fd) {
// This is the same check that Android does in EventHub.cpp return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
return hasRelAxis(fd, EvdevEvent.REL_X) && }
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) { // Returns the bytes read or -1 on error
// This is the same check that Android does in EventHub.cpp private static native int read(int fd, byte[] buffer);
return hasKey(fd, EvdevEvent.KEY_Q);
}
public static boolean isGamepad(int fd) { // Takes a byte buffer to use to read the output into.
return hasKey(fd, EvdevEvent.BTN_GAMEPAD); // This buffer MUST be in native byte order and at least
} // EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead);
return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
// Returns the bytes read or -1 on error buffer.limit(bytesRead);
private static native int read(int fd, byte[] buffer); buffer.rewind();
// Takes a byte buffer to use to read the output into. // Throw away the time stamp
// This buffer MUST be in native byte order and at least if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
// EVDEV_MAX_EVENT_SIZE bytes long. buffer.getLong();
public static EvdevEvent read(int fd, ByteBuffer buffer) { buffer.getLong();
int bytesRead = read(fd, buffer.array()); } else {
if (bytesRead < 0) { buffer.getInt();
LimeLog.warning("Failed to read: "+bytesRead); buffer.getInt();
return null; }
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead); return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
buffer.rewind(); }
// Throw away the time stamp // Closes the fd from open()
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { public static native int close(int fd);
buffer.getLong();
buffer.getLong();
} else {
buffer.getInt();
buffer.getInt();
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
}
// Closes the fd from open()
public static native int close(int fd);
} }
@@ -0,0 +1,116 @@
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,136 +4,136 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = { private 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,
KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7, KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS, KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q, KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T, KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y, KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET, KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET, KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G, KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON, KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE, KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE, KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH, KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z, KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X, KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH, KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT, KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY, KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK, KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1, KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2, KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3, KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4, KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5, KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6, KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7, KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8, KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9, KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10, KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK, KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK, KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6, KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD, KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_NUMPAD_DOT,
0, 0,
0, //KeyEvent.VK_ZENKAKUHANKAKU, 0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND, 0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11, KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12, KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO, 0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA, 0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA, 0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN, 0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA, 0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN, 0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA, 0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT, KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE, KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ, KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT, KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED, 0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME, KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END, KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT, KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL, KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO, 0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE, 0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN, 0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP, 0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */ 0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS, KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS, 0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK, KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */ 0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
}; };
public static short translateEvdevKeyCode(short evdevKeyCode) { public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) { if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode]; return EVDEV_KEY_CODES[evdevKeyCode];
} }
return 0; return 0;
} }
} }
@@ -10,163 +10,179 @@ import android.os.FileObserver;
@SuppressWarnings("ALL") @SuppressWarnings("ALL")
public class EvdevWatcher { public class EvdevWatcher {
private static final String PATH = "/dev/input"; private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event"; private static final String REQUIRED_FILE_PREFIX = "event";
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>(); private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false; private boolean shutdown = false;
private boolean init = false; private boolean init = false;
private boolean ungrabbed = false; private boolean ungrabbed = false;
private EvdevListener listener; private EvdevListener listener;
private Thread startThread; private Thread startThread;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { private static boolean patchedSeLinuxPolicies = false;
@Override
public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
return; @Override
} public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
synchronized (handlers) { if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
if (shutdown) { return;
return; }
}
if ((event & FileObserver.CREATE) != 0) { synchronized (handlers) {
LimeLog.info("Starting evdev handler for "+fileName); if (shutdown) {
return;
}
if (!init) { if ((event & FileObserver.CREATE) != 0) {
// If this a real new device, update permissions again so we can read it LimeLog.info("Starting evdev handler for "+fileName);
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); if (!init) {
// If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
// If we're ungrabbed now, don't start the handler EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
if (!ungrabbed) {
handler.start();
}
handlers.put(fileName, handler); // If we're ungrabbed now, don't start the handler
} if (!ungrabbed) {
handler.start();
}
if ((event & FileObserver.DELETE) != 0) { handlers.put(fileName, handler);
LimeLog.info("Halting evdev handler for "+fileName); }
EvdevHandler handler = handlers.remove(fileName); if ((event & FileObserver.DELETE) != 0) {
if (handler != null) { LimeLog.info("Halting evdev handler for "+fileName);
handler.notifyDeleted();
}
}
}
}
};
public EvdevWatcher(EvdevListener listener) { EvdevHandler handler = handlers.remove(fileName);
this.listener = listener; if (handler != null) {
} handler.notifyDeleted();
}
}
}
}
};
private File[] rundownWithPermissionsChange(int newPermissions) { public EvdevWatcher(EvdevListener listener) {
// Rundown existing files this.listener = listener;
File devInputDir = new File(PATH); }
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
// Set desired permissions private File[] rundownWithPermissionsChange(int newPermissions) {
String[] filePaths = new String[files.length]; // Rundown existing files
for (int i = 0; i < files.length; i++) { File devInputDir = new File(PATH);
filePaths[i] = files[i].getAbsolutePath(); File[] files = devInputDir.listFiles();
} if (files == null) {
EvdevReader.setPermissions(filePaths, newPermissions); return new File[0];
}
return files; // Set desired permissions
} String[] filePaths = new String[files.length];
for (int i = 0; i < files.length; i++) {
filePaths[i] = files[i].getAbsolutePath();
}
EvdevReader.setPermissions(filePaths, newPermissions);
public void ungrabAll() { return files;
synchronized (handlers) { }
// Note that we're ungrabbed for now
ungrabbed = true;
// Stop all handlers public void ungrabAll() {
for (EvdevHandler handler : handlers.values()) { synchronized (handlers) {
handler.stop(); // Note that we're ungrabbed for now
} ungrabbed = true;
}
}
public void regrabAll() { // Stop all handlers
synchronized (handlers) { for (EvdevHandler handler : handlers.values()) {
// We're regrabbing everything now handler.stop();
ungrabbed = false; }
}
}
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) { public void regrabAll() {
// We need to recreate each entry since we can't reuse a stopped one synchronized (handlers) {
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); // We're regrabbing everything now
entry.getValue().start(); ungrabbed = false;
}
}
}
public void start() { for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
startThread = new Thread() { // We need to recreate each entry since we can't reuse a stopped one
@Override entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
public void run() { entry.getValue().start();
// List all files and allow us access }
File[] files = rundownWithPermissionsChange(0666); }
}
init = true; public void start() {
for (File f : files) { startThread = new Thread() {
observer.onEvent(FileObserver.CREATE, f.getName()); @Override
} public void run() {
// Initialize the root shell
EvdevShell.getInstance().startShell();
// Done with initial onEvent calls // Patch SELinux policies (if needed)
init = false; if (!patchedSeLinuxPolicies) {
EvdevReader.patchSeLinuxPolicies();
patchedSeLinuxPolicies = true;
}
// Start watching for new files // List all files and allow us access
observer.startWatching(); File[] files = rundownWithPermissionsChange(0666);
synchronized (startThread) { init = true;
// Wait to be awoken again by shutdown() for (File f : files) {
try { observer.onEvent(FileObserver.CREATE, f.getName());
startThread.wait(); }
} catch (InterruptedException e) {}
}
// Giveup eventX permissions // Done with initial onEvent calls
rundownWithPermissionsChange(066); init = false;
}
};
startThread.start();
}
public void shutdown() { // Start watching for new files
// Let start thread cleanup on it's own sweet time observer.startWatching();
synchronized (startThread) {
startThread.notify();
}
// Stop the observer synchronized (startThread) {
observer.stopWatching(); // Wait to be awoken again by shutdown()
try {
startThread.wait();
} catch (InterruptedException e) {}
}
synchronized (handlers) { // Giveup eventX permissions
// Stop creating new handlers rundownWithPermissionsChange(0660);
shutdown = true;
// If we've already ungrabbed, there's nothing else to do // Kill the root shell
if (ungrabbed) { try {
return; EvdevShell.getInstance().stopShell();
} } catch (InterruptedException e) {}
}
};
startThread.start();
}
// Stop all handlers public void shutdown() {
for (EvdevHandler handler : handlers.values()) { // Let start thread cleanup on it's own sweet time
handler.stop(); synchronized (startThread) {
} startThread.notify();
} }
}
// Stop the observer
observer.stopWatching();
synchronized (handlers) {
// Stop creating new handlers
shutdown = true;
// If we've already ungrabbed, there's nothing else to do
if (ungrabbed) {
return;
}
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
} }
@@ -20,138 +20,140 @@ import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock") @SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer { public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private Thread rendererThread, decoderThread; private Thread rendererThread, decoderThread;
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 = 5;
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;
private static final int HIGH_PERF = 3; private static final int HIGH_PERF = 3;
private int totalFrames; private int totalFrames;
private long totalTimeMs; private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors(); private final int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused") @SuppressWarnings("unused")
private int findOptimalPerformanceLevel() { private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder(); StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null; BufferedReader br = null;
try { try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) { for (;;) {
int ch = br.read(); int ch = br.read();
if (ch == -1) if (ch == -1)
break; break;
cpuInfo.append((char)ch); cpuInfo.append((char)ch);
} }
// Here we're doing very simple heuristics based on CPU model // Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString(); String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection // We order them from greatest to least for proper detection
// of devices with multiple sets of cores (like Exynos 5 Octa) // of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM) // TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) { if (Build.FINGERPRINT.contains("generic")) {
// Emulator // Emulator
return LOW_PERF; return LOW_PERF;
} }
else if (cpuInfoStr.contains("0xc0f")) { else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15 // Cortex-A15
return MED_PERF; return MED_PERF;
} }
else if (cpuInfoStr.contains("0xc09")) { else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9 // Cortex-A9
return LOW_PERF; return LOW_PERF;
} }
else if (cpuInfoStr.contains("0xc07")) { else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7 // Cortex-A7
return LOW_PERF; return LOW_PERF;
} }
else { else {
// Didn't have anything we're looking for // Didn't have anything we're looking for
return MED_PERF; return MED_PERF;
} }
} catch (IOException e) { } catch (IOException e) {
} finally { } finally {
if (br != null) { if (br != null) {
try { try {
br.close(); br.close();
} catch (IOException e) {} } catch (IOException e) {}
} }
} }
// Couldn't read cpuinfo, so assume medium // Couldn't read cpuinfo, so assume medium
return MED_PERF; return MED_PERF;
} }
@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.targetFps = redrawRate; this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel(); int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount; int threadCount;
int avcFlags = 0; int avcFlags = 0;
switch (perfLevel) { switch (perfLevel) {
case HIGH_PERF: case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive // Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE; avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1; threadCount = 1;
break; break;
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.FAST_BILINEAR_FILTERING;
// 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;
break; break;
default: default:
case MED_PERF: case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING; avcFlags = AvcDecoder.BILINEAR_FILTERING;
// Only use 2 threads to minimize frame processing latency // Only use 2 threads to minimize frame processing latency
threadCount = 2; threadCount = 2;
break; break;
} }
// If the user wants quality, we'll remove the low IQ flags // If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) { if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled // Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER; avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations // Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE; avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding"); LimeLog.info("Using high quality decoding");
} }
SurfaceHolder sh = (SurfaceHolder)renderTarget; SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888); sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount); int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) { if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err); throw new IllegalStateException("AVC decoder initialization failure: "+err);
} }
AvcDecoder.setRenderTarget(sh.getSurface()); if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
return false;
}
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")"); LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true; return true;
} }
@Override @Override
public boolean start(final VideoDepacketizer depacketizer) { public boolean start(final VideoDepacketizer depacketizer) {
decoderThread = new Thread() { decoderThread = new Thread() {
@Override @Override
public void run() { public void run() {
@@ -172,112 +174,112 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
decoderThread.setPriority(Thread.MAX_PRIORITY - 1); decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
decoderThread.start(); decoderThread.start();
rendererThread = new Thread() { rendererThread = new Thread() {
@Override @Override
public void run() { public void run() {
long nextFrameTime = System.currentTimeMillis(); long nextFrameTime = System.currentTimeMillis();
DecodeUnit du; DecodeUnit du;
while (!isInterrupted()) while (!isInterrupted())
{ {
long diff = nextFrameTime - System.currentTimeMillis(); long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) { if (diff > WAIT_CEILING_MS) {
try { try {
Thread.sleep(diff - WAIT_CEILING_MS); Thread.sleep(diff - WAIT_CEILING_MS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
return; return;
} }
continue; continue;
} }
nextFrameTime = computePresentationTimeMs(targetFps); nextFrameTime = computePresentationTimeMs(targetFps);
AvcDecoder.redraw(); AvcDecoder.redraw();
} }
} }
}; };
rendererThread.setName("Video - Renderer (CPU)"); rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY); rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start(); rendererThread.start();
return true; return true;
} }
private long computePresentationTimeMs(int frameRate) { private long computePresentationTimeMs(int frameRate) {
return System.currentTimeMillis() + (1000 / frameRate); return System.currentTimeMillis() + (1000 / frameRate);
} }
@Override @Override
public void stop() { public void stop() {
rendererThread.interrupt(); rendererThread.interrupt();
decoderThread.interrupt(); decoderThread.interrupt();
try { try {
rendererThread.join(); rendererThread.join();
} catch (InterruptedException e) { } } catch (InterruptedException e) { }
try { try {
decoderThread.join(); decoderThread.join();
} catch (InterruptedException e) { } } catch (InterruptedException e) { }
} }
@Override @Override
public void release() { public void release() {
AvcDecoder.destroy(); AvcDecoder.destroy();
} }
private boolean submitDecodeUnit(DecodeUnit decodeUnit) { private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
byte[] data; byte[] data;
// Use the reserved decoder buffer if this decode unit will fit // Use the reserved decoder buffer if this decode unit will fit
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear(); decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length); decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
} }
data = decoderBuffer.array(); data = decoderBuffer.array();
} }
else { else {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()]; data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0; int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
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;
} }
} }
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0); boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) { if (success) {
long timeAfterDecode = System.currentTimeMillis(); 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 < 300) { if (delta >= 0 && delta < 1000) {
totalTimeMs += delta; totalTimeMs += delta;
totalFrames++; totalFrames++;
} }
} }
return success; return success;
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return 0; return 0;
} }
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
return 0; return 0;
} }
@Override @Override
public int getAverageEndToEndLatency() { public int getAverageEndToEndLatency() {
if (totalFrames == 0) { if (totalFrames == 0) {
return 0; return 0;
} }
return (int)(totalTimeMs / totalFrames); return (int)(totalTimeMs / totalFrames);
} }
@Override @Override
public String getDecoderName() { public String getDecoderName() {
@@ -5,75 +5,75 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer { public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
private EnhancedDecoderRenderer decoderRenderer; private EnhancedDecoderRenderer decoderRenderer;
@Override @Override
public void release() { public void release() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
decoderRenderer.release(); decoderRenderer.release();
} }
} }
@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) {
if (decoderRenderer == null) { if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags); return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
} }
public void initializeWithFlags(int drFlags) { public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 || if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 && ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) { MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer(); decoderRenderer = new MediaCodecDecoderRenderer();
} }
else { else {
decoderRenderer = new AndroidCpuDecoderRenderer(); decoderRenderer = new AndroidCpuDecoderRenderer();
} }
} }
public boolean isHardwareAccelerated() { public boolean isHardwareAccelerated() {
if (decoderRenderer == null) { if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return (decoderRenderer instanceof MediaCodecDecoderRenderer); return (decoderRenderer instanceof MediaCodecDecoderRenderer);
} }
@Override @Override
public boolean start(VideoDepacketizer depacketizer) { public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer); return decoderRenderer.start(depacketizer);
} }
@Override @Override
public void stop() { public void stop() {
decoderRenderer.stop(); decoderRenderer.stop();
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return decoderRenderer.getCapabilities(); return decoderRenderer.getCapabilities();
} }
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency(); return decoderRenderer.getAverageDecoderLatency();
} }
else { else {
return 0; return 0;
} }
} }
@Override @Override
public int getAverageEndToEndLatency() { public int getAverageEndToEndLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency(); return decoderRenderer.getAverageEndToEndLatency();
} }
else { else {
return 0; return 0;
} }
} }
@Override @Override
public String getDecoderName() { public String getDecoderName() {
@@ -25,415 +25,432 @@ import android.view.SurfaceHolder;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer { public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers; private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder; private MediaCodec videoDecoder;
private Thread rendererThread; private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4; private boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer; private VideoDepacketizer depacketizer;
private boolean adaptivePlayback; private boolean adaptivePlayback;
private int initialWidth, initialHeight; private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack; private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps; private SeqParameterSet savedSps;
private long lastTimestampUs; private long lastTimestampUs;
private long totalTimeMs; private long totalTimeMs;
private long decoderTimeMs; private long decoderTimeMs;
private int totalFrames; private int totalFrames;
private String decoderName; private String decoderName;
private int numSpsIn; private int numSpsIn;
private int numPpsIn; private int numPpsIn;
private int numIframeIn; private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false; private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() { public MediaCodecDecoderRenderer() {
//dumpDecoders(); //dumpDecoders();
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder(); MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
if (decoder == null) { if (decoder == null) {
decoder = MediaCodecHelper.findFirstDecoder(); decoder = MediaCodecHelper.findFirstDecoder();
} }
if (decoder == null) { if (decoder == null) {
// This case is handled later in setup() // This case is handled later in setup()
return; return;
} }
decoderName = decoder.getName(); decoderName = decoder.getName();
// Set decoder-specific attributes // Set decoder-specific attributes
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); needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
isExynos4 = MediaCodecHelper.isExynos4Device(); 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) { if (needsBaselineSpsHack) {
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack"); 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");
} }
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP) @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;
this.initialHeight = height; this.initialHeight = height;
if (decoderName == null) { if (decoderName == null) {
LimeLog.severe("No available hardware decoder!"); LimeLog.severe("No available hardware decoder!");
return false; return false;
} }
// Codecs have been known to throw all sorts of crazy runtime exceptions // Codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems // due to implementation problems
try { try {
videoDecoder = MediaCodec.createByCodecName(decoderName); videoDecoder = MediaCodec.createByCodecName(decoderName);
} catch (Exception e) { } catch (Exception e) {
return false; return false;
} }
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height); MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices // Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat // so we don't fill these pre-KitKat
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width); videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width);
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 // 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) { if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() { videoDecoder.setCallback(new MediaCodec.Callback() {
@Override @Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed"); LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format); LimeLog.info("New output Format: " + format);
} }
@Override @Override
public void onOutputBufferAvailable(MediaCodec codec, int index, public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) { BufferInfo info) {
try { try {
// FIXME: It looks like we can't frameskip here // FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true); codec.releaseOutputBuffer(index, true);
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
@Override @Override
public void onInputBufferAvailable(MediaCodec codec, int index) { public void onInputBufferAvailable(MediaCodec codec, int index) {
try { try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index); submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) { } catch (InterruptedException e) {
// What do we do here? // What do we do here?
e.printStackTrace(); e.printStackTrace();
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
@Override @Override
public void onError(MediaCodec codec, CodecException e) { public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) { if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo()); LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace(); e.printStackTrace();
} }
else { else {
LimeLog.severe(e.getDiagnosticInfo()); LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace(); 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);
LimeLog.info("Using hardware decoding"); LimeLog.info("Using hardware decoding");
return true; return true;
} }
@TargetApi(Build.VERSION_CODES.LOLLIPOP) @TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) { 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;
if (codecExc.isTransient()) { if (codecExc.isTransient()) {
// We'll let transient exceptions go // We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo()); LimeLog.warning(codecExc.getDiagnosticInfo());
return; return;
} }
LimeLog.severe(codecExc.getDiagnosticInfo()); LimeLog.severe(codecExc.getDiagnosticInfo());
} }
} }
if (buf != null || codecFlags != 0) { if (buf != null || codecFlags != 0) {
throw new RendererException(dr, e, buf, codecFlags); throw new RendererException(dr, e, buf, codecFlags);
} }
else { else {
throw new RendererException(dr, e); throw new RendererException(dr, e);
} }
} }
private void startRendererThread() private void startRendererThread()
{ {
rendererThread = new Thread() { rendererThread = new Thread() {
@SuppressWarnings("deprecation") @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;
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,
// try to submit up to 5 decode units at once without blocking. // try to submit up to 5 decode units at once without blocking.
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 = videoDecoder.dequeueInputBuffer(0); inputIndex = videoDecoder.dequeueInputBuffer(0);
du = depacketizer.pollNextDecodeUnit(); du = depacketizer.pollNextDecodeUnit();
// 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, videoDecoderInputBuffers[inputIndex], 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(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
// Grab an input buffer if we don't have one already. // Grab an input buffer if we don't have one already.
// This way we can have one ready hopefully by the time // This way we can have one ready hopefully by the time
// the depacketizer is done with this frame. It's important // the depacketizer is done with this frame. It's important
// that this can timeout because it's possible that we could exhaust // that this can timeout because it's possible that we could exhaust
// the decoder's input buffers and deadlocks because aren't pulling // the decoder's input buffers and deadlocks because aren't pulling
// frames out of the other end. // frames out of the other end.
if (inputIndex == -1) { if (inputIndex == -1) {
try { try {
// 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 = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0); inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
} catch (Exception e) { } catch (Exception e) {
inputIndex = -1; inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, 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 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) {
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex); submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
// DU and input buffer have both been consumed // DU and input buffer have both been consumed
du = null; du = null;
inputIndex = -1; inputIndex = -1;
} }
// Try to output a frame // Try to output a frame
try { try {
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0); int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
if (outIndex >= 0) { if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs; long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex; int lastIndex = outIndex;
// Get the last output buffer in the queue // Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false); videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex; lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs; presentationTimeUs = info.presentationTimeUs;
} }
// Render the last buffer // Render the last buffer
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 = System.currentTimeMillis()-(presentationTimeUs/1000); long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta >= 0 && delta < 300) { if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta; decoderTimeMs += delta;
totalTimeMs += delta; totalTimeMs += delta;
} }
} else { } else {
switch (outIndex) { switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER: case MediaCodec.INFO_TRY_AGAIN_LATER:
// Getting an input buffer may already block // Getting an input buffer may already block
// so don't park if we still need to do that // so don't park if we still need to do that
if (inputIndex >= 0) { if (inputIndex >= 0) {
LockSupport.parkNanos(1); LockSupport.parkNanos(1);
} }
break; break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed"); LimeLog.info("Output buffers changed");
break; 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());
break; break;
default: default:
break; break;
} }
} }
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0); handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
} }
} }
} }
}; };
rendererThread.setName("Video - Renderer (MediaCodec)"); rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.MAX_PRIORITY); rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start(); rendererThread.start();
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @Override
public boolean start(VideoDepacketizer depacketizer) { public boolean start(VideoDepacketizer depacketizer) {
this.depacketizer = depacketizer; this.depacketizer = depacketizer;
// Start the decoder // Start the decoder
videoDecoder.start(); videoDecoder.start();
// On devices pre-Lollipop, we'll use a rendering thread // On devices pre-Lollipop, we'll use a rendering thread
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers(); videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread(); startRendererThread();
} }
return true; return true;
} }
@Override @Override
public void stop() { public void stop() {
if (rendererThread != null) { if (rendererThread != null) {
// Halt the rendering thread // Halt the rendering thread
rendererThread.interrupt(); rendererThread.interrupt();
try { try {
rendererThread.join(); rendererThread.join();
} catch (InterruptedException ignored) { } } catch (InterruptedException ignored) { }
} }
// Stop the decoder // Stop the decoder
videoDecoder.stop(); videoDecoder.stop();
} }
@Override @Override
public void release() { public void release() {
if (videoDecoder != null) { if (videoDecoder != null) {
videoDecoder.release(); videoDecoder.release();
} }
} }
private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) { private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
// Try 25 times to submit the input buffer before throwing a real exception // Try 25 times to submit the input buffer before throwing a real exception
int i; int i;
Exception lastException = null; Exception lastException = null;
for (i = 0; i < 25; i++) { for (i = 0; i < 25; i++) {
try { try {
videoDecoder.queueInputBuffer(inputBufferIndex, videoDecoder.queueInputBuffer(inputBufferIndex,
0, length, 0, length,
timestampUs, codecFlags); timestampUs, codecFlags);
break; break;
} catch (Exception e) { } catch (Exception e) {
handleDecoderException(this, e, null, codecFlags); handleDecoderException(this, e, null, codecFlags);
lastException = e; lastException = e;
} }
} }
if (i == 25) { if (i == 25) {
throw new RendererException(this, lastException, null, codecFlags); throw new RendererException(this, lastException, null, codecFlags);
} }
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp(); long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) { if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp(); totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++; totalFrames++;
} }
long timestampUs = currentTime * 1000; 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
timestampUs = lastTimestampUs + 1; timestampUs = lastTimestampUs + 1;
} }
lastTimestampUs = timestampUs; lastTimestampUs = timestampUs;
// Clear old input data // Clear old input data
buf.clear(); buf.clear();
int codecFlags = 0; int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags(); int decodeUnitFlags = decodeUnit.getFlags();
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG; codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
} }
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) { if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME; codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
numIframeIn++; numIframeIn++;
} }
boolean needsSpsReplay = false; boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) { if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0); ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
if (header.data[header.offset+4] == 0x67) { if (header.data[header.offset+4] == 0x67) {
numSpsIn++; numSpsIn++;
ByteBuffer spsBuf = ByteBuffer.wrap(header.data); ByteBuffer spsBuf = ByteBuffer.wrap(header.data);
// Skip to the start of the NALU data // Skip to the start of the NALU data
spsBuf.position(header.offset+5); spsBuf.position(header.offset+5);
SeqParameterSet sps = SeqParameterSet.read(spsBuf); SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 // Some decoders rely on H264 level to decide how many buffers are needed
// also requires this fixup. // Since we only need one frame buffered, we'll set the level as low as we can
// // for known resolution combinations
// I'm doing this fixup for all devices because I haven't seen any devices that if (initialWidth == 1280 && initialHeight == 720) {
// this causes issues for. At worst, it seems to do nothing and at best it fixes // Max 5 buffered frames at 1280x720x60
// issues with video lag, hangs, and crashes. LimeLog.info("Patching level_idc to 32");
LimeLog.info("Patching num_ref_frames in SPS"); sps.level_idc = 32;
sps.num_ref_frames = 1; }
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)
}
if (needsSpsBitstreamFixup || isExynos4) { // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag // also requires this fixup.
// or max_dec_frame_buffering which increases decoding latency on Tegra. //
LimeLog.info("Adding bitstream restrictions"); // I'm doing this fixup for all devices because I haven't seen any devices that
// this causes issues for. At worst, it seems to do nothing and at best it fixes
// issues with video lag, hangs, and crashes.
LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1;
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction(); if (needsSpsBitstreamFixup || isExynos4) {
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true; // The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2; // or max_dec_frame_buffering which increases decoding latency on Tegra.
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1; LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16; sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0; sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1; 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_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
}
// If we need to hack this SPS to say we're baseline, do so now // If we need to hack this SPS to say we're baseline, do so now
if (needsBaselineSpsHack) { if (needsBaselineSpsHack) {
@@ -442,20 +459,20 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
savedSps = sps; savedSps = sps;
} }
// Write the annex B header // Write the annex B header
buf.put(header.data, header.offset, 5); buf.put(header.data, header.offset, 5);
// Write the modified SPS to the input buffer // Write the modified SPS to the input buffer
sps.write(buf); sps.write(buf);
queueInputBuffer(inputBufferIndex, queueInputBuffer(inputBufferIndex,
0, buf.position(), 0, buf.position(),
timestampUs, codecFlags); timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit); depacketizer.freeDecodeUnit(decodeUnit);
return; return;
} else if (header.data[header.offset+4] == 0x68) { } else if (header.data[header.offset+4] == 0x68) {
numPpsIn++; numPpsIn++;
if (needsBaselineSpsHack) { if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack"); LimeLog.info("Saw PPS; disabling SPS hack");
@@ -464,25 +481,25 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Give the decoder the SPS again with the proper profile now // Give the decoder the SPS again with the proper profile now
needsSpsReplay = true; 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.getBufferList()) for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
{ {
buf.put(desc.data, desc.offset, desc.length); buf.put(desc.data, desc.offset, desc.length);
} }
queueInputBuffer(inputBufferIndex, queueInputBuffer(inputBufferIndex,
0, decodeUnit.getDataLength(), 0, decodeUnit.getDataLength(),
timestampUs, codecFlags); timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit); depacketizer.freeDecodeUnit(decodeUnit);
if (needsSpsReplay) { if (needsSpsReplay) {
replaySps(); replaySps();
} }
} }
private void replaySps() { private void replaySps() {
int inputIndex = videoDecoder.dequeueInputBuffer(-1); int inputIndex = videoDecoder.dequeueInputBuffer(-1);
@@ -511,27 +528,27 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
LimeLog.info("SPS replay complete"); LimeLog.info("SPS replay complete");
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return adaptivePlayback ? return adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0; VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
} }
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
if (totalFrames == 0) { if (totalFrames == 0) {
return 0; return 0;
} }
return (int)(decoderTimeMs / totalFrames); return (int)(decoderTimeMs / totalFrames);
} }
@Override @Override
public int getAverageEndToEndLatency() { public int getAverageEndToEndLatency() {
if (totalFrames == 0) { if (totalFrames == 0) {
return 0; return 0;
} }
return (int)(totalTimeMs / totalFrames); return (int)(totalTimeMs / totalFrames);
} }
@Override @Override
public String getDecoderName() { public String getDecoderName() {
@@ -539,62 +556,62 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} }
public class RendererException extends RuntimeException { public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L; private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException; private final Exception originalException;
private MediaCodecDecoderRenderer renderer; private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer; private ByteBuffer currentBuffer;
private int currentCodecFlags; private int currentCodecFlags;
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) { public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.renderer = renderer; this.renderer = renderer;
this.originalException = e; this.originalException = e;
} }
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) { public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.renderer = renderer; this.renderer = renderer;
this.originalException = e; this.originalException = e;
this.currentBuffer = currentBuffer; this.currentBuffer = currentBuffer;
this.currentCodecFlags = currentCodecFlags; this.currentCodecFlags = currentCodecFlags;
} }
public String toString() { public String toString() {
String str = ""; String str = "";
str += "Decoder: "+renderer.decoderName+"\n"; str += "Decoder: "+renderer.decoderName+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n"; str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n"; str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n"; str += "Total frames: "+renderer.totalFrames+"\n";
if (currentBuffer != null) { if (currentBuffer != null) {
str += "Current buffer: "; str += "Current buffer: ";
currentBuffer.flip(); currentBuffer.flip();
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) { while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
str += String.format((Locale)null, "%02x ", currentBuffer.get()); str += String.format((Locale)null, "%02x ", currentBuffer.get());
} }
str += "\n"; str += "\n";
str += "Buffer codec flags: "+currentCodecFlags+"\n"; str += "Buffer codec flags: "+currentCodecFlags+"\n";
} }
str += "Is Exynos 4: "+renderer.isExynos4+"\n"; str += "Is Exynos 4: "+renderer.isExynos4+"\n";
str += "/proc/cpuinfo:\n"; str += "/proc/cpuinfo:\n";
try { try {
str += MediaCodecHelper.readCpuinfo(); str += MediaCodecHelper.readCpuinfo();
} catch (Exception e) { } catch (Exception e) {
str += e.getMessage(); str += e.getMessage();
} }
str += "Full decoder dump:\n"; str += "Full decoder dump:\n";
try { try {
str += MediaCodecHelper.dumpDecoders(); str += MediaCodecHelper.dumpDecoders();
} catch (Exception e) { } catch (Exception e) {
str += e.getMessage(); str += e.getMessage();
} }
str += originalException.toString(); str += originalException.toString();
return str; return str;
} }
} }
} }
@@ -20,12 +20,12 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
public static final List<String> preferredDecoders; private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes; private static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes; private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes; private static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes; private static final List<String> baselineProfileHackPrefixes;
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -44,6 +44,7 @@ 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 = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel"); baselineProfileHackPrefixes.add("omx.intel");
@@ -71,8 +72,8 @@ public class MediaCodecHelper {
@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 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 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)");
@@ -145,7 +146,7 @@ public class MediaCodecHelper {
return str; return str;
} }
public static MediaCodecInfo findPreferredDecoder() { private 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
@@ -216,7 +217,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")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception { private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -17,153 +17,153 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager { public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db"; private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers"; private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp"; private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac"; private static final String MAC_COLUMN_NAME = "Mac";
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) { public ComputerDatabaseManager(Context c) {
try { try {
// Create or open an existing DB // Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) { } catch (SQLiteException e) {
// Delete the DB and try again // Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME); c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} }
initializeDb(); initializeDb();
} }
public void close() { public void close() {
computerDb.close(); computerDb.close();
} }
private void initializeDb() { private void initializeDb() {
// Create tables if they aren't already there // Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," + computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)", " %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME, COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME)); REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
} }
public void deleteComputer(String name) { public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
} }
public boolean updateComputer(ComputerDetails details) { public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress()); values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress()); values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress); values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
public List<ComputerDetails> getAllComputers() { public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>(); LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
while (c.moveToNext()) { while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
details.name = c.getString(0); details.name = c.getString(0);
String uuidStr = c.getString(1); String uuidStr = c.getString(1);
try { try {
details.uuid = UUID.fromString(uuidStr); details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name); LimeLog.severe("DB: Corrupted UUID for "+details.name);
} }
try { try {
details.localIp = InetAddress.getByAddress(c.getBlob(2)); details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name); LimeLog.severe("DB: Corrupted local IP for "+details.name);
} }
try { try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name); LimeLog.severe("DB: Corrupted remote IP for "+details.name);
} }
details.macAddress = c.getString(4); details.macAddress = c.getString(4);
// 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; 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 ||
details.macAddress == null) { details.macAddress == null) {
continue; continue;
} }
computerList.add(details); computerList.add(details);
} }
c.close(); c.close();
return computerList; return computerList;
} }
public ComputerDetails getComputerByName(String name) { public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
if (!c.moveToFirst()) { if (!c.moveToFirst()) {
// No matching computer // No matching computer
c.close(); c.close();
return null; return null;
} }
details.name = c.getString(0); details.name = c.getString(0);
String uuidStr = c.getString(1); String uuidStr = c.getString(1);
try { try {
details.uuid = UUID.fromString(uuidStr); details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name); LimeLog.severe("DB: Corrupted UUID for "+details.name);
} }
try { try {
details.localIp = InetAddress.getByAddress(c.getBlob(2)); details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name); LimeLog.severe("DB: Corrupted local IP for "+details.name);
} }
try { try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name); LimeLog.severe("DB: Corrupted remote IP for "+details.name);
} }
details.macAddress = c.getString(4); details.macAddress = c.getString(4);
c.close(); c.close();
details.state = ComputerDetails.State.UNKNOWN; details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.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) {
deleteComputer(details.name); deleteComputer(details.name);
return null; return null;
} }
return details; return details;
} }
} }
@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener { public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details); public void notifyComputerUpdated(ComputerDetails details);
} }
@@ -1,16 +1,23 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
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;
@@ -19,46 +26,46 @@ 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 POLLING_PERIOD_MS = 3000; private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int MDNS_QUERY_PERIOD_MS = 1000;
private ComputerManagerBinder binder = new ComputerManagerBinder(); private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0); private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false; private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
synchronized (discoveryServiceConnection) { synchronized (discoveryServiceConnection) {
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder); DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
// Set us as the event listener // Set us as the event listener
privateBinder.setListener(createDiscoveryListener()); privateBinder.setListener(createDiscoveryListener());
// Signal a possible waiter that we're all setup // Signal a possible waiter that we're all setup
discoveryBinder = privateBinder; discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll(); discoveryServiceConnection.notifyAll();
} }
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
discoveryBinder = null; discoveryBinder = null;
} }
}; };
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details) private boolean runPoll(ComputerDetails details, boolean newPc)
{ {
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -103,7 +110,7 @@ public class ComputerManagerService extends Service {
public void run() { public void run() {
while (!isInterrupted() && pollingActive) { while (!isInterrupted() && pollingActive) {
// Check if this poll has modified the details // Check if this poll has modified the details
runPoll(details); runPoll(details, false);
// Wait until the next polling interval // Wait until the next polling interval
try { try {
@@ -118,16 +125,16 @@ public class ComputerManagerService extends Service {
return t; return t;
} }
public class ComputerManagerBinder extends Binder { public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) { public void startPolling(ComputerManagerListener listener) {
// Polling is active // Polling is active
pollingActive = true; 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) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
@@ -141,56 +148,68 @@ public class ComputerManagerService extends Service {
} }
} }
} }
} }
public void waitForReady() { public void waitForReady() {
synchronized (discoveryServiceConnection) { synchronized (discoveryServiceConnection) {
try { try {
while (discoveryBinder == null) { while (discoveryBinder == null) {
// Wait for the bind notification // Wait for the bind notification
discoveryServiceConnection.wait(1000); discoveryServiceConnection.wait(1000);
} }
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
} }
} }
public void waitForPollingStopped() { public void waitForPollingStopped() {
while (activePolls.get() != 0) { while (activePolls.get() != 0) {
try { try {
Thread.sleep(250); Thread.sleep(250);
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
} }
} }
public boolean addComputerBlocking(InetAddress addr) { public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr); return ComputerManagerService.this.addComputerBlocking(addr);
} }
public void addComputer(InetAddress addr) { public void removeComputer(String name) {
ComputerManagerService.this.addComputer(addr); ComputerManagerService.this.removeComputer(name);
} }
public void removeComputer(String name) { public void stopPolling() {
ComputerManagerService.this.removeComputer(name); // Just call the unbind handler to cleanup
} ComputerManagerService.this.onUnbind(null);
}
public void stopPolling() { public ApplistPoller createAppListPoller(ComputerDetails computer) {
// Just call the unbind handler to cleanup return new ApplistPoller(computer);
ComputerManagerService.this.onUnbind(null); }
}
public String getUniqueId() { public String getUniqueId() {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
}
@Override public ComputerDetails getComputer(UUID uuid) {
public boolean onUnbind(Intent intent) { synchronized (pollingTuples) {
// Stop mDNS autodiscovery for (PollingTuple tuple : pollingTuples) {
discoveryBinder.stopDiscovery(); if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
// Stop polling return null;
}
}
@Override
public boolean onUnbind(Intent intent) {
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
// Stop polling
pollingActive = false; pollingActive = false;
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
@@ -202,51 +221,42 @@ public class ComputerManagerService extends Service {
} }
} }
// Remove the listener // Remove the listener
listener = null; listener = null;
return false; return false;
} }
private MdnsDiscoveryListener createDiscoveryListener() { private MdnsDiscoveryListener createDiscoveryListener() {
return new MdnsDiscoveryListener() { return new MdnsDiscoveryListener() {
@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
addComputer(computer.getAddress()); addComputerBlocking(computer.getAddress());
} }
@Override @Override
public void notifyComputerRemoved(MdnsComputer computer) { public void notifyComputerRemoved(MdnsComputer computer) {
// Nothing to do here // Nothing to do here
} }
@Override @Override
public void notifyDiscoveryFailure(Exception e) { public void notifyDiscoveryFailure(Exception e) {
LimeLog.severe("mDNS discovery failed"); LimeLog.severe("mDNS discovery failed");
e.printStackTrace(); e.printStackTrace();
} }
}; };
} }
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
addTuple(fakeDetails);
}
private void addTuple(ComputerDetails details) { private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
if (tuple.computer == details || if (tuple.computer.uuid.equals(details.uuid)) {
tuple.computer.localIp.equals(details.localIp) || // Update details anyway in case this machine has been re-added by IP
tuple.computer.remoteIp.equals(details.remoteIp) || // after not being reachable by our existing information
tuple.computer.name.equals(details.name)) { tuple.computer.localIp = details.localIp;
tuple.computer.remoteIp = details.remoteIp;
// Start a polling thread if polling is active // Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) { if (pollingActive && tuple.thread == null) {
@@ -268,18 +278,19 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(InetAddress addr) { public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder // Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr; fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr; fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details // Block while we try to fill the details
runPoll(fakeDetails); runPoll(fakeDetails, true);
// If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
// Start a polling thread for this machine // Start a polling thread for this machine
addTuple(fakeDetails); addTuple(fakeDetails);
return true; return true;
@@ -287,15 +298,15 @@ public class ComputerManagerService extends Service {
else { else {
return false; return false;
} }
} }
public void removeComputer(String name) { public void removeComputer(String name) {
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return; return;
} }
// Remove it from the database // Remove it from the database
dbManager.deleteComputer(name); dbManager.deleteComputer(name);
synchronized (pollingTuples) { synchronized (pollingTuples) {
// Remove the computer from the computer list // Remove the computer from the computer list
@@ -311,76 +322,92 @@ public class ComputerManagerService extends Service {
} }
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
} }
private boolean getLocalDatabaseReference() { private boolean getLocalDatabaseReference() {
if (dbRefCount.get() == 0) { if (dbRefCount.get() == 0) {
return false; return false;
} }
dbRefCount.incrementAndGet(); dbRefCount.incrementAndGet();
return true; return true;
} }
private void releaseLocalDatabaseReference() { private void releaseLocalDatabaseReference() {
if (dbRefCount.decrementAndGet() == 0) { if (dbRefCount.decrementAndGet() == 0) {
dbManager.close(); dbManager.close();
} }
} }
private ComputerDetails tryPollIp(InetAddress ipAddr) { private ComputerDetails tryPollIp(ComputerDetails details, 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));
return http.getComputerDetails(); ComputerDetails newDetails = http.getComputerDetails();
} catch (Exception e) {
return null;
}
}
private boolean pollComputer(ComputerDetails details, boolean localFirst) { // Check if this is the PC we expected
ComputerDetails polledDetails; 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;
}
if (localFirst) { return newDetails;
polledDetails = tryPollIp(details.localIp); } catch (Exception e) {
} return null;
else { }
polledDetails = tryPollIp(details.remoteIp); }
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { private boolean pollComputer(ComputerDetails details, boolean localFirst) {
// Failed, so let's try the fallback ComputerDetails polledDetails;
if (!localFirst) {
polledDetails = tryPollIp(details.localIp);
}
else {
polledDetails = tryPollIp(details.remoteIp);
}
// The fallback poll worked // If the local address is routable across the Internet,
if (polledDetails != null) { // always consider this PC remote to be conservative
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL : if (details.localIp.equals(details.remoteIp)) {
ComputerDetails.Reachability.REMOTE; localFirst = false;
} }
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries if (localFirst) {
if (polledDetails == null) { polledDetails = tryPollIp(details, details.localIp);
return false; }
} else {
polledDetails = tryPollIp(details, details.remoteIp);
}
// If we got here, it's reachable if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
details.update(polledDetails); // Failed, so let's try the fallback
return true; if (!localFirst) {
} polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
private boolean doPollMachine(ComputerDetails details) { // The fallback poll worked
if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries
if (polledDetails == null) {
return false;
}
// If we got here, it's reachable
details.update(polledDetails);
return true;
}
private boolean doPollMachine(ComputerDetails details) {
if (details.reachability == ComputerDetails.Reachability.UNKNOWN || if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
details.reachability == ComputerDetails.Reachability.OFFLINE) { details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Always try local first to avoid potential UDP issues when // Always try local first to avoid potential UDP issues when
@@ -393,20 +420,20 @@ public class ComputerManagerService extends Service {
// always try that one first // always try that one first
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL); return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
} }
} }
@Override @Override
public void onCreate() { public void onCreate() {
// Bind to the discovery service // Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class), bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE); discoveryServiceConnection, Service.BIND_AUTO_CREATE);
// 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 // Grab known machines into our computer list
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
@@ -414,35 +441,136 @@ public class ComputerManagerService extends Service {
} }
for (ComputerDetails computer : dbManager.getAllComputers()) { for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add this computer without a thread // Add tuples for each computer
pollingTuples.add(new PollingTuple(computer, null)); addTuple(computer);
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
if (discoveryBinder != null) { if (discoveryBinder != null) {
// Unbind from the discovery service // Unbind from the discovery service
unbindService(discoveryServiceConnection); unbindService(discoveryServiceConnection);
} }
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection // FIXME: Should await termination here but we have timeout issues in HttpURLConnection
// Remove the initial DB reference // Remove the initial DB reference
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
} }
@Override @Override
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(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
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
cacheOut.close();
// 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 { class PollingTuple {
public Thread thread; public Thread thread;
public ComputerDetails computer; public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) { public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer; this.computer = computer;
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
import android.content.Context; import android.content.Context;
public class IdentityManager { public class IdentityManager {
private static final String UNIQUE_ID_FILE_NAME = "uniqueid"; private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8; private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId; private String uniqueId;
public IdentityManager(Context c) { public IdentityManager(Context c) {
uniqueId = loadUniqueId(c); uniqueId = loadUniqueId(c);
if (uniqueId == null) { if (uniqueId == null) {
uniqueId = generateNewUniqueId(c); uniqueId = generateNewUniqueId(c);
} }
LimeLog.info("UID is now: "+uniqueId); LimeLog.info("UID is now: "+uniqueId);
} }
public String getUniqueId() { public String getUniqueId() {
return uniqueId; return uniqueId;
} }
private static String loadUniqueId(Context c) { private static String loadUniqueId(Context c) {
// 2 Hex digits per byte // 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2]; char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null; InputStreamReader reader = null;
LimeLog.info("Reading UID from disk"); LimeLog.info("Reading UID from disk");
try { try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME)); reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{ {
LimeLog.severe("UID file data is truncated"); LimeLog.severe("UID file data is truncated");
return null; return null;
} }
return new String(uid); return new String(uid);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
LimeLog.info("No UID file found"); LimeLog.info("No UID file found");
return null; return null;
} catch (IOException e) { } catch (IOException e) {
LimeLog.severe("Error while reading UID file"); LimeLog.severe("Error while reading UID file");
e.printStackTrace(); e.printStackTrace();
return null; return null;
} finally { } finally {
if (reader != null) { if (reader != null) {
try { try {
reader.close(); reader.close();
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
} }
private static String generateNewUniqueId(Context c) { private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string // Generate a new UID hex string
LimeLog.info("Generating new UID"); LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong()); String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null; OutputStreamWriter writer = null;
try { try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0)); writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr); writer.write(uidStr);
LimeLog.info("UID written to disk"); LimeLog.info("UID written to disk");
} catch (IOException e) { } catch (IOException e) {
LimeLog.severe("Error while writing UID file"); LimeLog.severe("Error while writing UID file");
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
if (writer != null) { if (writer != null) {
try { try {
writer.close(); writer.close();
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
// We can return a UID even if I/O fails // We can return a UID even if I/O fails
return uidStr; return uidStr;
} }
} }
@@ -16,75 +16,75 @@ import android.os.IBinder;
public class DiscoveryService extends Service { public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent; private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener; private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock; private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder { public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) { public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener; boundListener = listener;
} }
public void startDiscovery(int queryIntervalMs) { public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire(); multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs); discoveryAgent.startDiscovery(queryIntervalMs);
} }
public void stopDiscovery() { public void stopDiscovery() {
discoveryAgent.stopDiscovery(); discoveryAgent.stopDiscovery();
multicastLock.release(); multicastLock.release();
} }
public List<MdnsComputer> getComputerSet() { public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet(); return discoveryAgent.getComputerSet();
} }
} }
@Override @Override
public void onCreate() { public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false); multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() { discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyComputerAdded(computer); boundListener.notifyComputerAdded(computer);
} }
} }
@Override @Override
public void notifyComputerRemoved(MdnsComputer computer) { public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyComputerRemoved(computer); boundListener.notifyComputerRemoved(computer);
} }
} }
@Override @Override
public void notifyDiscoveryFailure(Exception e) { public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e); boundListener.notifyDiscoveryFailure(e);
} }
} }
}); });
} }
private DiscoveryBinder binder = new DiscoveryBinder(); private final DiscoveryBinder binder = new DiscoveryBinder();
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
@Override @Override
public boolean onUnbind(Intent intent) { public boolean onUnbind(Intent intent) {
// Stop any discovery session // Stop any discovery session
discoveryAgent.stopDiscovery(); discoveryAgent.stopDiscovery();
multicastLock.release(); multicastLock.release();
// Unbind the listener // Unbind the listener
boundListener = null; boundListener = null;
return false; return false;
} }
} }
@@ -1,16 +1,25 @@
package com.limelight.grid; package com.limelight.grid;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion; import com.koushikdutta.ion.Ion;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R; import com.limelight.R;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.security.KeyManagementException; import java.security.KeyManagementException;
@@ -18,7 +27,10 @@ import java.security.NoSuchAlgorithmException;
import java.security.Principal; import java.security.Principal;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
@@ -32,16 +44,16 @@ import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> { public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private InetAddress address; private final ComputerDetails computer;
private String uniqueId; private final String uniqueId;
private LimelightCryptoProvider cryptoProvider; private final LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext; private final SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>(); private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, R.layout.app_grid_item, R.drawable.image_loading); super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.address = address; this.computer = computer;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context); cryptoProvider = PlatformBinding.getCryptoProvider(context);
@@ -50,7 +62,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
} }
TrustManager[] trustAllCerts = new TrustManager[] { private final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; return new X509Certificate[0];
@@ -59,7 +71,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public void checkServerTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}}; }};
KeyManager[] ourKeyman = new KeyManager[] { private final KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() { new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes, public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { Principal[] issuers, Socket socket) {
@@ -94,8 +106,27 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public boolean verify(String hostname, SSLSession session) { return true; } public boolean verify(String hostname, SSLSession session) { return true; }
}; };
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());
}
});
}
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) { public void addApp(AppView.AppObject app) {
itemList.add(app); itemList.add(app);
sortList();
} }
public void abortPendingRequests() { public void abortPendingRequests() {
@@ -107,7 +138,9 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
for (Future f : tempMap.values()) { for (Future f : tempMap.values()) {
f.cancel(true); if (!f.isCancelled() && !f.isDone()) {
f.cancel(true);
}
} }
synchronized (pendingRequests) { synchronized (pendingRequests) {
@@ -118,30 +151,25 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
} }
@Override // TODO: Handle pruning of bitmap cache
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) { private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
try {
// Set SSL contexts correctly to allow us to authenticate // PNG ignores quality setting
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts); FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext); bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
out.close();
// Set off the deferred image load } catch (IOException e) {
synchronized (pendingRequests) { e.printStackTrace();
Future f = Ion.with(imgView)
.placeholder(defaultImageRes)
.error(defaultImageRes)
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
.setCallback(new FutureCallback<ImageView>() {
@Override
public void onCompleted(Exception e, ImageView result) {
synchronized (pendingRequests) {
pendingRequests.remove(imgView);
}
}
});
pendingRequests.put(imgView, f);
} }
}
@Override
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
// Clear existing contents of the image view
imgView.setAlpha(0.0f);
// Check the on-disk cache
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
return true; return true;
} }
@@ -166,4 +194,96 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay // No overlay
return false; return false;
} }
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private final ImageView view;
private final int appId;
public ImageCacheRequest(ImageView view, int appId) {
this.view = view;
this.appId = appId;
}
@Override
protected Bitmap doInBackground(Void... v) {
InputStream in = null;
try {
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
return BitmapFactory.decodeStream(in);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
return null;
}
private void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(250).start();
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
// Disk cache was read successfully
LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")");
view.setImageBitmap(result);
fadeInImage(view);
}
else {
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0");
// Load the placeholder image
view.setImageResource(defaultImageRes);
fadeInImage(view);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
// Kick off the deferred image load
synchronized (pendingRequests) {
Future<Bitmap> f = Ion.with(context)
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0")
.asBitmap()
.setCallback(new FutureCallback<Bitmap>() {
@Override
public void onCompleted(Exception e, final Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
fadeInImage(view);
// Populate the disk cache if we got an image back.
// We do it in a new thread because it can be very expensive, especially
// when we do the initial load where lots of disk I/O is happening at once.
new Thread() {
@Override
public void run() {
populateBitmapCache(computer.uuid, appId, result);
}
}.start();
}
else {
// Leave the loading icon as is (probably should change this eventually...)
}
}
});
pendingRequests.put(view, f);
}
}
}
}
} }
@@ -13,11 +13,11 @@ import com.limelight.R;
import java.util.ArrayList; import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter { public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected Context context; protected final Context context;
protected int defaultImageRes; protected final int defaultImageRes;
protected int layoutId; protected final int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>(); protected final ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater; protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context; this.context = context;
@@ -60,17 +60,21 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay); ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text); TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
if (!populateImageView(imgView, itemList.get(i))) { if (imgView != null) {
imgView.setImageResource(defaultImageRes); if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes);
}
} }
if (!populateTextView(txtView, itemList.get(i))) { if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString()); txtView.setText(itemList.get(i).toString());
} }
if (!populateOverlayView(overlayView, itemList.get(i))) { if (overlayView != null) {
overlayView.setVisibility(View.INVISIBLE); if (!populateOverlayView(overlayView, itemList.get(i))) {
} overlayView.setVisibility(View.INVISIBLE);
else { }
overlayView.setVisibility(View.VISIBLE); else {
overlayView.setVisibility(View.VISIBLE);
}
} }
return convertView; return convertView;
@@ -8,14 +8,27 @@ import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> { public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context) { public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, R.layout.pc_grid_item, R.drawable.computer); 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) { public void addComputer(PcView.ComputerObject computer) {
itemList.add(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) { public boolean removeComputer(PcView.ComputerObject computer) {
@@ -1,44 +1,44 @@
package com.limelight.nvstream.av.video.cpu; package com.limelight.nvstream.av.video.cpu;
public class AvcDecoder { public class AvcDecoder {
static { static {
// FFMPEG dependencies // FFMPEG dependencies
System.loadLibrary("avutil-52"); System.loadLibrary("avutil-52");
System.loadLibrary("swresample-0"); System.loadLibrary("swresample-0");
System.loadLibrary("swscale-2"); System.loadLibrary("swscale-2");
System.loadLibrary("avcodec-55"); System.loadLibrary("avcodec-55");
System.loadLibrary("avformat-55"); System.loadLibrary("avformat-55");
System.loadLibrary("nv_avc_dec"); System.loadLibrary("nv_avc_dec");
} }
/** Disables the deblocking filter at the cost of image quality */ /** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1; public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */ /** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2; public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */ /** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4; public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */ /** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8; public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */ /** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10; public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */ /** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20; public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */ /** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40; public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount); public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy(); public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0 // Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface); public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize); public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize); public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw(); public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1 // Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize(); public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen); public static native int decode(byte[] indata, int inoff, int inlen);
} }
@@ -2,6 +2,7 @@ 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;
@@ -13,151 +14,168 @@ 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.KeyEvent;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
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 TextView hostText; private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread; private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) { public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread(); startAddThread();
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
joinAddThread(); joinAddThread();
managerBinder = null; managerBinder = null;
} }
}; };
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), SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false); 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 = getResources().getString(R.string.addpc_fail);
} }
else { else {
msg = getResources().getString(R.string.addpc_success); msg = getResources().getString(R.string.addpc_success);
finish = true; finish = true;
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host); msg = getResources().getString(R.string.addpc_unknown_host);
} }
dialog.dismiss(); 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() {
@Override @Override
public void run() { public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show(); Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) { if (toastFinish && !isFinishing()) {
// Close the activity // Close the activity
AddComputerManually.this.finish(); AddComputerManually.this.finish();
} }
} }
}); });
} }
private void startAddThread() { private void startAddThread() {
addThread = new Thread() { addThread = new Thread() {
@Override @Override
public void run() { public void run() {
while (!isInterrupted()) { while (!isInterrupted()) {
String computer; String computer;
try { try {
computer = computersToAdd.take(); computer = computersToAdd.take();
} catch (InterruptedException e) { } catch (InterruptedException e) {
return; return;
} }
doAddPc(computer); doAddPc(computer);
} }
} }
}; };
addThread.setName("UI - AddComputerManually"); addThread.setName("UI - AddComputerManually");
addThread.start(); addThread.start();
} }
private void joinAddThread() { private void joinAddThread() {
if (addThread != null) { if (addThread != null) {
addThread.interrupt(); addThread.interrupt();
try { try {
addThread.join(); addThread.join();
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
addThread = null; addThread = null;
} }
} }
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
Dialog.closeDialogs(); Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
if (managerBinder != null) { if (managerBinder != null) {
joinAddThread(); joinAddThread();
unbindService(serviceConnection); unbindService(serviceConnection);
} }
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_computer_manually); String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
this.hostText = (TextView) findViewById(R.id.hostTextView); this.hostText = (TextView) findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE); hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() { hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override @Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE || if (actionId == EditorInfo.IME_ACTION_DONE ||
keyEvent.getAction() == KeyEvent.ACTION_DOWN && (keyEvent != null &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) { keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) { if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show(); Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true; return true;
} }
computersToAdd.add(hostText.getText().toString()); 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; 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);
} }
} }
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
@@ -13,11 +14,15 @@ public class PreferenceConfiguration {
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 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 = 30; private static final int BITRATE_DEFAULT_1080_60 = 20;
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";
@@ -27,6 +32,9 @@ public class PreferenceConfiguration {
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; 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;
@@ -37,6 +45,8 @@ public class PreferenceConfiguration {
public int decoder; public int decoder;
public int deadzonePercentage; 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")) {
@@ -57,6 +67,17 @@ public class PreferenceConfiguration {
} }
} }
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
// TVs shouldn't use small mode by default
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);
@@ -135,11 +156,16 @@ public class PreferenceConfiguration {
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE); 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;
} }
@@ -20,10 +20,14 @@ public class SeekBarPreference extends DialogPreference
private SeekBar seekBar; private SeekBar seekBar;
private TextView valueText; private TextView valueText;
private Context context; private final Context context;
private String dialogMessage, suffix; private final String dialogMessage;
private int defaultValue, maxValue, minValue, currentValue; private final String suffix;
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);
@@ -127,13 +131,6 @@ 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) {
@@ -0,0 +1,21 @@
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,20 +1,32 @@
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 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()
@@ -23,6 +35,16 @@ public class StreamSettings extends Activity {
UiHelper.notifyNewRootView(this); 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 {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -0,0 +1,35 @@
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));
}
}
@@ -0,0 +1,8 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -0,0 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -0,0 +1,55 @@
package com.limelight.utils;
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 FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
}
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);
}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,70 +7,71 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private String title, message; private final String title;
private Activity activity; private final String message;
private boolean endAfterDismiss; private final Activity activity;
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>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss) private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.endAfterDismiss = endAfterDismiss; this.endAfterDismiss = endAfterDismiss;
} }
public static void closeDialogs() public static void closeDialogs()
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) { for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) { if (d.alert.isShowing()) {
d.alert.dismiss(); d.alert.dismiss();
} }
} }
rundownDialogs.clear(); rundownDialogs.clear();
} }
} }
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss) public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss)); activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
} }
@Override @Override
public void run() { public void run() {
// If we're dying, don't bother creating a dialog // If we're dying, don't bother creating a dialog
if (activity.isFinishing()) if (activity.isFinishing())
return; return;
alert = new AlertDialog.Builder(activity).create(); alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title); alert.setTitle(title);
alert.setMessage(message); alert.setMessage(message);
alert.setCancelable(false); alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false); alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this); rundownDialogs.remove(Dialog.this);
alert.dismiss(); alert.dismiss();
} }
if (endAfterDismiss) { if (endAfterDismiss) {
activity.finish(); activity.finish();
} }
} }
}); });
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.add(this); rundownDialogs.add(this);
alert.show(); alert.show();
} }
} }
} }
@@ -9,111 +9,112 @@ 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 String title, message; private final String title;
private Activity activity; private final String message;
private ProgressDialog progress; private final Activity activity;
private boolean finish; private ProgressDialog progress;
private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>(); private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish) private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.progress = null; this.progress = null;
this.finish = finish; this.finish = finish;
} }
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish) public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{ {
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish); SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner); activity.runOnUiThread(spinner);
return spinner; return spinner;
} }
public static void closeDialogs(Activity activity) public static void closeDialogs(Activity activity)
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator(); Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) { while (i.hasNext()) {
SpinnerDialog dialog = i.next(); SpinnerDialog dialog = i.next();
if (dialog.activity == activity) { if (dialog.activity == activity) {
i.remove(); i.remove();
if (dialog.progress.isShowing()) { if (dialog.progress.isShowing()) {
dialog.progress.dismiss(); dialog.progress.dismiss();
} }
} }
} }
} }
} }
public void dismiss() public void dismiss()
{ {
// Running again with progress != null will destroy it // Running again with progress != null will destroy it
activity.runOnUiThread(this); activity.runOnUiThread(this);
} }
public void setMessage(final String message) public void setMessage(final String message)
{ {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
progress.setMessage(message); progress.setMessage(message);
} }
}); });
} }
@Override @Override
public void run() { public void run() {
// If we're dying, don't bother doing anything // If we're dying, don't bother doing anything
if (activity.isFinishing()) { if (activity.isFinishing()) {
return; return;
} }
if (progress == null) if (progress == null)
{ {
progress = new ProgressDialog(activity); progress = new ProgressDialog(activity);
progress.setTitle(title); progress.setTitle(title);
progress.setMessage(message); progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this); progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable // If we want to finish the activity when this is killed, make it cancellable
if (finish) if (finish)
{ {
progress.setCancelable(true); progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false); progress.setCanceledOnTouchOutside(false);
} }
else else
{ {
progress.setCancelable(false); progress.setCancelable(false);
} }
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.add(this); rundownDialogs.add(this);
progress.show(); progress.show();
} }
} }
else else
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) { if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss(); progress.dismiss();
} }
} }
} }
} }
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.remove(this); rundownDialogs.remove(this);
} }
// This will only be called if finish was true, so we don't need to check again // This will only be called if finish was true, so we don't need to check again
activity.finish(); activity.finish();
} }
} }
+12 -12
View File
@@ -69,9 +69,6 @@ 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;
@@ -370,17 +367,20 @@ 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;
// Only clone this frame if the last frame was taken. yuv_frame = new_frame;
// This saves on extra copies for frames that don't get
// rendered.
if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
}
pthread_mutex_unlock(&mutex); pthread_mutex_unlock(&mutex);
// Free the old frame outside of the mutex
if (old_frame != NULL) {
av_frame_free(&old_frame);
}
} }
return err < 0 ? err : 0; return err < 0 ? err : 0;
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<stroke android:width="1dip" android:color="#ffffff"/>
</shape>
@@ -33,12 +33,10 @@
android:text="@string/searching_pc"/> android:text="@string/searching_pc"/>
</RelativeLayout> </RelativeLayout>
<GridView <FrameLayout
android:id="@+id/pcGridView" android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center" android:gravity="center"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
@@ -33,12 +33,10 @@
android:text="@string/searching_pc"/> android:text="@string/searching_pc"/>
</RelativeLayout> </RelativeLayout>
<GridView <FrameLayout
android:id="@+id/pcGridView" android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center" android:gravity="center"
android:layout_toLeftOf="@+id/manuallyAddPc" android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc" android:layout_toStartOf="@+id/manuallyAddPc"
@@ -8,21 +8,17 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" > tools:context=".AppView" >
<GridView <FrameLayout
android:id="@+id/appGridView" android:id="@+id/appFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:gravity="center" android:gravity="center"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText"> android:layout_below="@+id/appListText"/>
</GridView>
<TextView <TextView
android:id="@+id/appListText" android:id="@+id/appListText"
@@ -30,6 +26,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
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"/> android:textSize="28sp"/>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="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
@@ -0,0 +1,15 @@
<?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>
@@ -0,0 +1,15 @@
<?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
@@ -0,0 +1,19 @@
<?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>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="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
@@ -0,0 +1,14 @@
<?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>
@@ -0,0 +1,14 @@
<?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>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:padding="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/grid_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textIsSelectable="false"
android:textSize="16sp" >
</TextView>
</LinearLayout>
+17 -3
View File
@@ -56,6 +56,9 @@
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string> <string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string> <string name="searching_pc">Ricerca PC in corso…</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Applicazioni su</string> <string name="title_applist">Applicazioni su</string>
@@ -70,6 +73,7 @@
<string name="applist_quit_app">Chiusura in corso…</string> <string name="applist_quit_app">Chiusura in corso…</string>
<string name="applist_quit_success">Sessione chiusa con successo</string> <string name="applist_quit_success">Sessione chiusa con successo</string>
<string name="applist_quit_fail">Chiusura sessione fallita</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 --> <!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string> <string name="title_add_pc">Aggiungi PC Manualmente</string>
@@ -87,20 +91,30 @@
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</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="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string> <string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_disable_warnings">Disabilita i messaggi di warning</string> <string name="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="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="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="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</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 Limelight</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="category_host_settings">Impostazioni Host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</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="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="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo. Richiede GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string> <string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="title_decoder_list">Cambia decoder</string> <string name="title_decoder_list">Cambia decoder</string>
<string name="summary_decoder_list">Il decoder software può aumentare la latenza video quando si usano impostazioni streaming basse</string> <string name="summary_decoder_list">Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse</string>
</resources> </resources>
+11
View File
@@ -13,6 +13,17 @@
<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>Auto-select Decoder</item>
<item>Force Software Decoding</item> <item>Force Software Decoding</item>
+15 -1
View File
@@ -56,6 +56,9 @@
<!-- 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="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 --> <!-- AppList activity -->
<string name="title_applist">Apps on</string> <string name="title_applist">Apps on</string>
@@ -70,6 +73,7 @@
<string name="applist_quit_app">Quitting</string> <string name="applist_quit_app">Quitting</string>
<string name="applist_quit_success">Successfully quit</string> <string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to 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="title_add_pc">Add PC Manually</string>
@@ -91,14 +95,24 @@
<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="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="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_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 Limelight</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. Requires GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Play audio from the computer and this device</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>
+27 -4
View File
@@ -26,14 +26,19 @@
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"> <PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference <!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone" android:key="seekbar_deadzone"
android:defaultValue="15" android:defaultValue="15"
android:max="50" android:max="50"
android:text="@string/suffix_seekbar_deadzone" android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/> android:title="@string/title_seekbar_deadzone"/-->
</PreferenceCategory--> <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"
@@ -46,6 +51,24 @@
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"
+1 -2
View File
@@ -1,6 +1,5 @@
<?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="Limelight" /> <application android:label="Limelight" />
+1 -2
View File
@@ -1,6 +1,5 @@
<?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" />
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.13.2' classpath 'com.android.tools.build:gradle:1.0.1'
} }
} }
+2 -2
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 - Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't 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 or first I-frame 4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only - 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
+2 -2
View File
@@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013 #Sun Dec 07 22:52:07 PST 2014
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.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip