Compare commits

..

107 Commits

Author SHA1 Message Date
Cameron Gutman 971263c52d Update common 2014-11-06 22:14:12 -08:00
Cameron Gutman 9b58e7bb4d Fix right clicking inconsistency on different devices 2014-11-06 20:38:29 -08:00
Cameron Gutman 69ecf0251d Forgot one activity 2014-11-06 20:07:59 -08:00
Cameron Gutman 350a4d8825 Add a helper class to perform initial UI fixups (currently adding padding on TV devices) 2014-11-06 20:07:01 -08:00
Cameron Gutman 44f447df7b Remove some old layout cruft 2014-11-06 20:01:32 -08:00
Cameron Gutman e8c4df4897 Add new Material-style launcher assets 2014-11-06 09:29:28 -08:00
Cameron Gutman 5ee16124bc Add new banners from phantom-playR 2014-11-05 18:50:40 -08:00
Cameron Gutman 8702ac72f0 Update readme 2014-11-05 18:43:16 -08:00
Cameron Gutman 004552ec30 Update version 2014-11-02 21:24:52 -08:00
Cameron Gutman 2f28400234 Update option text 2014-11-02 21:20:01 -08:00
Cameron Gutman 78d213d686 Fix thread spawning issue and remove some dead code 2014-11-02 21:16:09 -08:00
Cameron Gutman 1a71dda243 Add support for local audio playback mode 2014-11-02 20:55:15 -08:00
Cameron Gutman 21822f259c Significantly improve speed of PC list updates 2014-11-02 14:30:06 -08:00
Cameron Gutman 4f79607015 Fix full-screen theme 2014-11-02 13:41:35 -08:00
Cameron Gutman d8576d4c50 Auto-adjust bitrate when resolution/FPS changes 2014-11-02 13:25:43 -08:00
Cameron Gutman 2f4042da8f Pull preferences into their own class 2014-11-02 13:05:17 -08:00
Cameron Gutman c1397e331b Finish GUI for all preferences supported by the old preferences views 2014-11-02 12:10:21 -08:00
Cameron Gutman cd182b3265 Begin work on new preferences UI and massive code cleanup of settings-related activities 2014-10-30 01:27:43 -07:00
Cameron Gutman 28f2d7b84a Remove lint.xml 2014-10-30 00:25:32 -07:00
Cameron Gutman e8de7908fd Fix a bunch of static analysis warnings 2014-10-30 00:21:34 -07:00
Cameron Gutman 419c4c5592 Fix warning in JNI code 2014-10-29 23:57:11 -07:00
Cameron Gutman a9a8346f58 Fix app label 2014-10-29 23:42:37 -07:00
Cameron Gutman 7e1b3f861f Remove superfluous manifest information 2014-10-29 23:12:31 -07:00
Cameron Gutman f4204e1268 Add missing version info 2014-10-29 23:05:34 -07:00
Cameron Gutman 60f35cd0aa Start of work to get both root and non-root versions building in the same branch/project 2014-10-29 22:47:47 -07:00
Cameron Gutman bbcdaa94a1 Remove and ignore compiled JNI libraries 2014-10-29 21:55:54 -07:00
Cameron Gutman 8f6e8c00ef Replace BouncyCastle and Jcodec jars with Maven repo dependencies 2014-10-29 21:52:13 -07:00
Cameron Gutman 24cb347b10 Add license back 2014-10-29 21:39:05 -07:00
Cameron Gutman d1b4e9464f Fix iml files 2014-10-29 21:19:17 -07:00
Cameron Gutman 18f7bfab7f Add some other files that weren't migrated with the project 2014-10-29 21:17:03 -07:00
Cameron Gutman d84b4bcf9a Initial migration to Android Studio 2014-10-29 21:16:09 -07:00
Cameron Gutman 57d919798a Merge branch 'root'
Conflicts:
	AndroidManifest.xml
2014-10-28 20:42:00 -07:00
Michelle Bergeron efeeebb0a2 fix grammar and limelight windows link 2014-10-23 01:33:20 -04:00
Cameron Gutman bc1409ba6c Increment version 2014-10-21 19:24:43 -04:00
Cameron Gutman 6f05b2af8a Attempt to detect Exynos 4 to apply the bitstream fixup code 2014-10-21 19:20:54 -04:00
Cameron Gutman d441bef33e Merge branch 'master' into root
Conflicts:
	libs/limelight-common.jar
2014-10-17 23:42:51 -07:00
Cameron Gutman 7b1f6ee483 Update common and disable the new renderer for now 2014-10-17 22:49:36 -07:00
Cameron Gutman 332960922a Small addendum to the timestamp fix 2014-10-17 15:54:07 -07:00
Cameron Gutman ac03f73cf9 Add new experimental decoder code for Lollipop's asynchronous MediaCodec capability 2014-10-17 15:51:50 -07:00
Cameron Gutman fa847ef2fc Ensure that no input buffers will ever be submitted with the same timestamp per SDK docs 2014-10-17 14:45:58 -07:00
Cameron Gutman b19360ac75 Suppress deprecation warnings for MediaCodecList APIs 2014-10-17 14:27:33 -07:00
Cameron Gutman 562569dc6b Fix evdev_reader build on NDK r10c and add updated libevdev_reader.so binaries 2014-10-17 14:14:47 -07:00
Cameron Gutman a18aa51f5a Merge branch 'master' into root
Conflicts:
	AndroidManifest.xml
	libs/limelight-common.jar
	res/values/strings.xml
2014-10-17 13:55:45 -07:00
Cameron Gutman b492ac43f8 Rebuild libraries with NDK r10c 2014-10-17 13:33:14 -07:00
Cameron Gutman 5bf3efb247 Update for Android 5.0 2014-10-17 11:54:59 -07:00
Cameron Gutman 2d833c32b0 Update decoder errata with testing results 2014-10-17 09:58:48 -07:00
Cameron Gutman 19bade01b8 Move to the improved axis scaling algorithm 2014-10-16 21:17:35 -07:00
Cameron Gutman 1b991ba432 More layout and manifest fixes. Notably, moving most hardcoded strings to strings.xml 2014-10-16 21:05:46 -07:00
Cameron Gutman 9c48850bb7 More layout and manifest changes for Android TV 2014-10-16 20:09:06 -07:00
Cameron Gutman 3e6f5ff11c Remove v11 styles. Add Android TV banner. Disable title bar on themes. 2014-10-15 22:39:35 -07:00
Cameron Gutman 57c3d8af8b Add experimental support for Xbox 360 controller dpad events on devices without proper mappings 2014-10-14 22:55:22 -07:00
Cameron Gutman 8530451c8b Only get motion ranges from joystick or gamepad sources. Fixes reading deadzones from the wrong source when dealing with multiple input sources in the same device with overlapping axis values. 2014-10-14 21:56:02 -07:00
Cameron Gutman 69a5c0b5b3 Add bitstream restrictions to MediaTek devices and update decoder-errata.txt 2014-10-12 14:43:59 -07:00
Cameron Gutman a7c36dcde6 Include video dimensions in RendererException 2014-10-12 13:32:34 -07:00
Cameron Gutman dff6fc21f4 Increase minimum deadzone to 15% in an attempt to fix a reported deadzone issue 2014-10-12 10:02:10 -07:00
Cameron Gutman 895e0250d9 Increment app version and fix some manifest issues 2014-10-12 09:50:37 -07:00
Cameron Gutman fd538cbaff Remove Samsung decoders from the list of bitstream_restrictions fixup devices 2014-10-11 21:48:56 -07:00
Cameron Gutman 27ce6fa203 Always patch num_ref_frames in the SPS to increase compatibility (read: attempt to fix some crashes on various Chinese SoCs) 2014-10-11 21:47:58 -07:00
Cameron Gutman 8efe194682 Merge branch 'master' into root 2014-10-10 22:55:37 -07:00
Cameron Gutman 947882d16f Fix modifier keys on root version 2014-10-10 22:53:34 -07:00
Cameron Gutman f07e927103 Merge branch 'master' into root
Conflicts:
	libs/limelight-common.jar
2014-10-10 22:46:25 -07:00
Cameron Gutman a61b85b494 Update common 2014-10-10 22:45:12 -07:00
Cameron Gutman 0c4a049a80 More performance optimizations by submitting a batch of decode units to get them to the hardware decoder ASAP 2014-10-10 17:51:39 -07:00
Cameron Gutman 8403101d0f Small performance optimization by only blocking on an input buffer if we've already got a DU 2014-10-09 19:05:57 -07:00
Cameron Gutman 47d47afd73 Update common 2014-10-07 23:18:08 -07:00
Cameron Gutman 1430801888 Fix a potential deadlock that could occur if the input buffers are exhausted. A couple other performance optimizations by overlapping some latency. 2014-10-07 23:17:32 -07:00
Cameron Gutman 6efc7e254b Don't prefer the newer Samsung decoder because it seems to cause issues on older devices 2014-10-07 23:16:07 -07:00
Cameron Gutman ace1339811 Check local IP first always to properly handle cases where we're behind a router that won't get UDP traffic back to us if we use the remote address 2014-10-07 23:01:39 -07:00
Cameron Gutman a5171a1701 Fix some localization issues (for Arabic at least) 2014-10-05 15:47:36 -07:00
Cameron Gutman a50211ab95 Merge branch 'master' into root 2014-10-04 18:43:27 -07:00
Cameron Gutman 31677adaa0 Add code to handle OUYA reporting invalid flat values 2014-10-04 18:41:18 -07:00
Cameron Gutman 247a19766c Merge branch 'master' into root 2014-10-03 23:35:12 -07:00
Cameron Gutman 1f09cbd609 Fix mouse scrolling and remove unreachable code 2014-10-03 23:20:09 -07:00
Cameron Gutman 731e4dc31e Merge branch 'master' into root 2014-10-03 23:09:40 -07:00
Cameron Gutman b6ee0764ff Clear connection state before stopping to avoid potential deadlocks 2014-10-03 23:07:42 -07:00
Cameron Gutman cd4bf9a28b Merge branch 'master' into root
Conflicts:
	AndroidManifest.xml
	libs/limelight-common.jar
	src/com/limelight/Game.java
2014-10-03 23:05:06 -07:00
Cameron Gutman f56b7ff79e Bump to 2.5.6 2014-10-03 22:56:30 -07:00
Cameron Gutman 645ea683ee Increase the minimum deadzone to 12% because I'm paranoid 2014-10-03 22:52:48 -07:00
Cameron Gutman 67e22fca6b Improve re-hiding of the system UI by using a proper system UI visibility listener 2014-10-03 22:49:36 -07:00
Cameron Gutman a726ba8ea7 Add an ungrab key combo (Ctrl+Shift+Z). Ignore repeat key down events. Fix some mishandling of input events that could cause crashes. 2014-10-03 22:18:36 -07:00
Cameron Gutman 23fcaa1bab Add axis scaling support 2014-10-01 20:24:40 -07:00
Cameron Gutman ad684a6f6b Merge version update 2014-09-28 16:38:56 -07:00
Cameron Gutman d3438f4938 Update common jar 2014-09-28 16:37:51 -07:00
Cameron Gutman cafdc21bf2 Prefer Samsung's OMX.SEC.AVC.Decoder if it's in the list of decoders 2014-09-28 16:37:41 -07:00
Cameron Gutman ceb9bd3342 Change bitstream restrictions to match default values 2014-09-28 16:37:30 -07:00
Cameron Gutman 196c0e6cbc Update version 2014-09-28 16:30:37 -07:00
Cameron Gutman e2cb7c953c Update common jar 2014-09-28 16:27:11 -07:00
Cameron Gutman 426b3c8522 Prefer Samsung's OMX.SEC.AVC.Decoder if it's in the list of decoders 2014-09-28 14:17:31 -07:00
Cameron Gutman 9648cf257f Change bitstream restrictions to match default values 2014-09-28 12:27:21 -07:00
Cameron Gutman 31d8687f67 Add failure tracing to EvdevReader 2014-09-27 20:02:10 -07:00
Cameron Gutman 991407a2cf Merge branch 'master' into root 2014-09-27 19:32:39 -07:00
Cameron Gutman 13b80eda8a Use a common cleanup function and stop input capturing after closing the connection to allow captured devices to accept the "Connection Failed" dialog 2014-09-27 19:32:10 -07:00
Cameron Gutman 6677949614 Update common jar 2014-09-27 15:49:59 -07:00
Cameron Gutman 080dcd92d7 Suppress a warning 2014-09-27 15:45:54 -07:00
Cameron Gutman 31b0bcf041 Fix manually adding PCs 2014-09-27 15:45:37 -07:00
Cameron Gutman 36664133f8 Speed up PC polling by only trying once if the remote and local IPs are the same 2014-09-27 15:43:43 -07:00
Cameron Gutman a3106bffca Speed up initial discovery by generating a new keypair while discovering machines. 2014-09-27 15:42:12 -07:00
Cameron Gutman 94a26fb831 Force CPU decoding to low performance to make the experience less horrible 2014-09-26 20:50:34 -07:00
Cameron Gutman 1b026f1354 Merge branch 'master' into root
Conflicts:
	AndroidManifest.xml
	src/com/limelight/binding/input/evdev/EvdevWatcher.java
2014-09-20 02:54:15 -07:00
Cameron Gutman 0517e8a530 Increment version and update common jar 2014-09-20 02:36:14 -07:00
Cameron Gutman a9fea34ac1 Add support for adaptive resolution changes. It's enabled by default on devices that claim support (KitKat+) or decoders that we know are okay. I pulled in jcodec to allow us to do proper SPS fixups without hardcoded offsets. 2014-09-19 22:25:38 -07:00
Cameron Gutman 201704dc9d Finally fix the random pairing failure. It turns out that it was causing by background querying for serverinfo during the pairing process. Now we stop polling computers while pairing is in progress. 2014-09-19 22:23:06 -07:00
Cameron Gutman 62ecb1af50 Use fixed point libopus builds on ARM and MIPS. This improves performance and allows the use of NEON on ARM for a huge perf boost 2014-09-17 19:36:51 -07:00
Cameron Gutman 9d4ca6293f Merge branch 'root' of github.com:limelight-stream/limelight-android into root 2014-09-17 19:31:07 -07:00
Cameron Gutman 2296b80edb Use fixed point libopus builds on ARM and MIPS. This improves performance and allows the use of NEON on ARM for a huge perf boost 2014-09-17 19:29:55 -07:00
Cameron Gutman 5577d48dcf Don't crash if no files are present in /dev/input 2014-09-15 18:50:38 -07:00
Cameron Gutman 819c5e823c Fix a bug where an error change any permissions would cause the operation to fail and other files to not be changed 2014-09-04 00:27:05 -07:00
583 changed files with 2948 additions and 4129 deletions
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
+35 -1
View File
@@ -1 +1,35 @@
/bin/*
#built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# Windows thumbnail db
Thumbs.db
# OSX files
.DS_Store
# Eclipse project files
.classpath
.project
# Android Studio
.idea
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
.gradle
build/
# Compiled JNI libraries folder
**/jniLibs
View File
@@ -1,2 +0,0 @@
*** SESSION Sep 21, 2013 18:55:11.17 -------------------------------------------
*** SESSION Sep 21, 2013 18:55:55.08 -------------------------------------------
@@ -1 +0,0 @@
@@ -1 +0,0 @@
@@ -1,3 +0,0 @@
com.android.ide.eclipse.adt.fixLegacyEditors=1
com.android.ide.eclipse.adt.sdk=C\:\\Users\\Andrew\\Desktop\\ADT\\adt-bundle-windows-x86_64-20130917\\sdk
eclipse.preferences.version=1
@@ -1,4 +0,0 @@
eclipse.preferences.version=1
spelling_locale_initialized=true
useAnnotationsPrefPage=true
useQuickDiffPrefPage=true
@@ -1,2 +0,0 @@
eclipse.preferences.version=1
version=1
@@ -1,13 +0,0 @@
content_assist_proposals_background=255,255,255
content_assist_proposals_foreground=0,0,0
eclipse.preferences.version=1
fontPropagated=true
org.eclipse.jdt.ui.editor.tab.width=
org.eclipse.jdt.ui.formatterprofiles.version=12
org.eclipse.jdt.ui.javadoclocations.migrated=true
org.eclipse.jface.textfont=1|Courier New|10.0|0|WINDOWS|1|0|0|0|0|0|0|0|0|1|0|0|0|0|Courier New;
proposalOrderMigrated=true
spelling_locale_initialized=true
tabWidthPropagated=true
useAnnotationsPrefPage=true
useQuickDiffPrefPage=true
@@ -1,5 +0,0 @@
PROBLEMS_FILTERS_MIGRATE=true
eclipse.preferences.version=1
platformState=1379804095671
quickStart=false
tipsAndTricks=true
@@ -1,2 +0,0 @@
eclipse.preferences.version=1
showIntro=false
File diff suppressed because it is too large Load Diff
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<typeInfoHistroy/>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<qualifiedTypeNameHistroy/>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench">
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
<item value="true" key="group_libraries"/>
<item value="false" key="linkWithEditor"/>
<item value="2" key="layout"/>
<item value="1" key="rootMode"/>
<item value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#x0D;&#x0A;&lt;packageExplorer group_libraries=&quot;1&quot; layout=&quot;2&quot; linkWithEditor=&quot;0&quot; rootMode=&quot;1&quot; workingSetName=&quot;&quot;&gt;&#x0D;&#x0A;&lt;customFilters userDefinedPatternsEnabled=&quot;false&quot;&gt;&#x0D;&#x0A;&lt;xmlDefinedFilters&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.StaticsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.FieldsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;/xmlDefinedFilters&gt;&#x0D;&#x0A;&lt;/customFilters&gt;&#x0D;&#x0A;&lt;/packageExplorer&gt;" key="memento"/>
</section>
<section name="JavaElementSearchActions">
</section>
</section>
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench">
<section name="ChooseWorkspaceDialogSettings">
<item value="185" key="DIALOG_Y_ORIGIN"/>
<item value="381" key="DIALOG_X_ORIGIN"/>
</section>
<section name="WORKBENCH_SETTINGS">
<list key="ENABLED_TRANSFERS">
</list>
</section>
<section name="ExternalProjectImportWizard">
<item value="false" key="WizardProjectsImportPage.STORE_ARCHIVE_SELECTED"/>
<item value="false" key="WizardProjectsImportPage.STORE_COPY_PROJECT_ID"/>
</section>
</section>
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench">
<section name="org.eclipse.ui.internal.QuickAccess">
<item value="1025" key="dialogWidth"/>
<item value="525" key="dialogHeight"/>
<list key="orderedProviders">
</list>
<list key="textArray">
</list>
<list key="orderedElements">
</list>
<list key="textEntries">
</list>
</section>
<section name="ImportExportAction">
</section>
</section>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<workingSetManager>
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804109849_0" label="Window Working Set" name="Aggregate for window 1379804109848"/>
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804153983_1" label="Window Working Set" name="Aggregate for window 1379804153983"/>
</workingSetManager>
-1
View File
@@ -1 +0,0 @@
org.eclipse.core.runtime=1
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Limelight</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
-11
View File
@@ -1,11 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6
+3 -7
View File
@@ -1,12 +1,12 @@
#Limelight
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software, and created a version that can be run on any Android device.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
in your own home, or over the internet.
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 Phone](https://github.com/limelight-stream/limelight-wp) 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.
##Features
@@ -14,9 +14,6 @@ in your own home, or over the internet.
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
##Features in development
* Keyboard input
##Installation
* Download and install Limelight for Android from
@@ -28,7 +25,6 @@ in your own home, or over the internet.
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
* Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended)
* Exynos/Snapdragon SoC __OR__ Quad-Core 1.4 GHz Cortex-A9 or higher (Tegra 3)
##Usage
+112
View File
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<exclude-output />
<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/aidl/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/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" />
</component>
</module>
+71
View File
@@ -0,0 +1,71 @@
import com.android.builder.model.ProductFlavor
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.0.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 21
versionName "2.9"
versionCode = 38
}
productFlavors {
root {
applicationId "com.limelight.root"
}
nonRoot {
applicationId "com.limelight"
}
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
sourceSets.main.jni.srcDirs = []
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def ndkDir = properties.getProperty('ndk.dir')
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine "$ndkDir\\ndk-build.cmd",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
else {
commandLine "$ndkDir/ndk-build",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
}
dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar')
}
@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight.root"
android:versionCode="33"
android:versionName="2.5.4.1" >
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="19" />
package="com.limelight">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -14,72 +8,72 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<!-- Launcher for traditional devices -->
<activity
android:name="com.limelight.PcView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="@string/app_name" >
android:name=".PcView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
</activity>
<!-- Launcher for Android TV devices -->
<activity
android:name="com.limelight.AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="App List" >
android:name=".PcViewTv"
android:logo="@drawable/atv_banner"
android:icon="@drawable/atv_banner"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name="com.limelight.StreamSettings"
android:name=".preferences.StreamSettings"
android:label="Streaming Settings" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name="com.limelight.AdvancedSettings"
android:label="Advanced Settings" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.StreamSettings" />
</activity>
<activity
android:name="com.limelight.AddComputerManually"
android:name=".preferences.AddComputerManually"
android:label="Add Computer Manually" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name="com.limelight.Game"
android:name=".Game"
android:screenOrientation="sensorLandscape"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="@string/title_activity_game"
android:parentActivityName="com.limelight.Connection"
android:theme="@style/FullscreenTheme" >
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.Connection" />
</activity>
<service
android:name="com.limelight.discovery.DiscoveryService"
android:name=".discovery.DiscoveryService"
android:label="mDNS PC Auto-Discovery Service" />
<service
android:name="com.limelight.computers.ComputerManagerService"
android:name=".computers.ComputerManagerService"
android:label="Computer Management Service" />
</application>
@@ -12,9 +12,10 @@ import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.root.R;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.content.Intent;
@@ -28,6 +29,7 @@ import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -51,6 +53,8 @@ public class AppView extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
@@ -59,7 +63,10 @@ public class AppView extends Activity {
return;
}
setTitle("App List for "+getIntent().getStringExtra(NAME_EXTRA));
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
try {
ipAddress = InetAddress.getByAddress(address);
@@ -77,7 +84,7 @@ public class AppView extends Activity {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appListAdapter.getItem(pos);
AppObject app = appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
@@ -130,7 +137,7 @@ public class AppView extends Activity {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appListAdapter.getItem(info.position);
AppObject selectedApp = appListAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
@@ -155,7 +162,7 @@ public class AppView extends Activity {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
AppObject app = (AppObject) appListAdapter.getItem(info.position);
AppObject app = appListAdapter.getItem(info.position);
switch (item.getItemId())
{
case RESUME_ID:
@@ -217,9 +224,9 @@ public class AppView extends Activity {
// Success case
return;
} catch (GfeHttpResponseException e) {
} catch (IOException e) {
} catch (XmlPullParserException e) {
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
@@ -14,14 +14,13 @@ import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.root.R;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Point;
import android.media.AudioManager;
import android.net.ConnectivityManager;
@@ -36,6 +35,7 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnGenericMotionListener;
import android.view.View.OnSystemUiVisibilityChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.Window;
@@ -44,7 +44,8 @@ import android.widget.Toast;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
@@ -56,8 +57,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private ControllerHandler controllerHandler;
private KeyboardTranslator keybTranslator;
private int height;
private int width;
private PreferenceConfiguration prefConfig;
private Point screenSize = new Point(0, 0);
private NvConnection conn;
@@ -66,10 +66,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean connecting = false;
private boolean connected = false;
private boolean stretchToFit;
private boolean toastsDisabled;
private EvdevWatcher evdevWatcher;
private int modifierFlags = 0;
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private ConfigurableDecoderRenderer decoderRenderer;
@@ -82,35 +82,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
public static final String PREFS_FILE_NAME = "gameprefs";
public static final String WIDTH_PREF_STRING = "ResH";
public static final String HEIGHT_PREF_STRING = "ResV";
public static final String REFRESH_RATE_PREF_STRING = "FPS";
public static final String DECODER_PREF_STRING = "Decoder";
public static final String BITRATE_PREF_STRING = "Bitrate";
public static final String STRETCH_PREF_STRING = "Stretch";
public static final String SOPS_PREF_STRING = "Sops";
public static final String DISABLE_TOASTS_PREF_STRING = "NoToasts";
public static final int BITRATE_DEFAULT_720_30 = 5;
public static final int BITRATE_DEFAULT_720_60 = 10;
public static final int BITRATE_DEFAULT_1080_30 = 10;
public static final int BITRATE_DEFAULT_1080_60 = 30;
public static final int DEFAULT_WIDTH = 1280;
public static final int DEFAULT_HEIGHT = 720;
public static final int DEFAULT_REFRESH_RATE = 60;
public static final int DEFAULT_DECODER = 0;
public static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
public static final boolean DEFAULT_STRETCH = false;
public static final boolean DEFAULT_SOPS = true;
public static final boolean DEFAULT_DISABLE_TOASTS = false;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -134,6 +105,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
}
// Listen for UI visibility events
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
// Change volume button behavior
setVolumeControlStream(AudioManager.STREAM_MUSIC);
@@ -144,31 +118,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
// Read the stream preferences
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
case Game.FORCE_SOFTWARE_DECODER:
prefConfig = PreferenceConfiguration.readPreferences(this);
switch (prefConfig.decoder) {
case PreferenceConfiguration.FORCE_SOFTWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
break;
case Game.AUTOSELECT_DECODER:
case PreferenceConfiguration.AUTOSELECT_DECODER:
break;
case Game.FORCE_HARDWARE_DECODER:
case PreferenceConfiguration.FORCE_HARDWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING;
break;
}
stretchToFit = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
if (stretchToFit) {
if (prefConfig.stretchVideo) {
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
}
int refreshRate, bitrate;
boolean sops;
width = prefs.getInt(WIDTH_PREF_STRING, DEFAULT_WIDTH);
height = prefs.getInt(HEIGHT_PREF_STRING, DEFAULT_HEIGHT);
refreshRate = prefs.getInt(REFRESH_RATE_PREF_STRING, DEFAULT_REFRESH_RATE);
bitrate = prefs.getInt(BITRATE_PREF_STRING, DEFAULT_BITRATE);
sops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
toastsDisabled = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
Display display = getWindowManager().getDefaultDisplay();
display.getSize(screenSize);
@@ -190,21 +154,30 @@ public class Game extends Activity implements SurfaceHolder.Callback,
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this,
new StreamConfiguration(app, width, height, refreshRate, bitrate * 1000, sops),
PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn);
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(app)
.setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.build();
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn);
SurfaceHolder sh = sv.getHolder();
if (stretchToFit || !decoderRenderer.isHardwareAccelerated()) {
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
// Set the surface to the size of the video
sh.setFixedSize(width, height);
sh.setFixedSize(prefConfig.width, prefConfig.height);
}
// Initialize touch contexts
@@ -267,11 +240,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
};
private void hideSystemUi() {
private void hideSystemUi(int delay) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.removeCallbacks(hideSystemUi);
h.postDelayed(hideSystemUi, 1000);
h.postDelayed(hideSystemUi, delay);
}
}
@@ -283,7 +256,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
Dialog.closeDialogs();
displayedFailureDialog = true;
conn.stop();
stopConnection();
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
@@ -301,10 +274,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
if (evdevWatcher != null) {
evdevWatcher.shutdown();
}
finish();
}
@@ -316,6 +285,84 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.release();
}
private Runnable toggleGrab = new Runnable() {
@Override
public void run() {
if (evdevWatcher != null) {
if (grabbedInput) {
evdevWatcher.ungrabAll();
}
else {
evdevWatcher.regrabAll();
}
}
grabbedInput = !grabbedInput;
}
};
// Returns true if the key stroke was consumed
private boolean handleSpecialKeys(short translatedKey, boolean down) {
int modifierMask = 0;
// Mask off the high byte
translatedKey &= 0xff;
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
modifierMask = KeyboardPacket.MODIFIER_CTRL;
}
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
}
else if (translatedKey == KeyboardTranslator.VK_ALT) {
modifierMask = KeyboardPacket.MODIFIER_ALT;
}
if (down) {
this.modifierFlags |= modifierMask;
}
else {
this.modifierFlags &= ~modifierMask;
}
// Check if Ctrl+Shift+Z is pressed
if (translatedKey == KeyboardTranslator.VK_Z &&
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
{
if (down) {
// Now that we've pressed the magic combo
// we'll wait for one of the keys to come up
grabComboDown = true;
}
else {
// Toggle the grab if Z comes up
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
}
return true;
}
// Toggle the grab if control or shift comes up
else if (grabComboDown) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
return true;
}
// Not a special combo
return false;
}
private static byte getModifierState(KeyEvent event) {
byte modifier = 0;
if (event.isShiftPressed()) {
@@ -330,6 +377,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return modifier;
}
private byte getModifierState() {
return (byte) modifierFlags;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
@@ -351,6 +402,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return super.onKeyDown(keyCode, event);
}
// Let this method take duplicate key down events
if (handleSpecialKeys(translated, true)) {
return true;
}
// Eat repeat down events
if (event.getRepeatCount() > 0) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyDown(keyCode, event);
}
keybTranslator.sendKeyDown(translated,
getModifierState(event));
}
@@ -360,16 +426,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Pressing a volume button drops the immersive flag so the UI shows up again and doesn't
// go away. I'm not sure if that's a bug or a feature, but we're working around it here
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.removeCallbacks(hideSystemUi);
h.postDelayed(hideSystemUi, 2000);
}
}
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyUp(keyCode, event);
@@ -389,6 +445,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return super.onKeyUp(keyCode, event);
}
if (handleSpecialKeys(translated, false)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyUp(keyCode, event);
}
keybTranslator.sendKeyUp(translated,
getModifierState(event));
}
@@ -405,25 +470,35 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return null;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
// Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) {
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return false;
}
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (controllerHandler.handleMotionEvent(event)) {
return true;
}
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
{
// This case is for touch-based input devices
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
event.getSource() == InputDevice.SOURCE_STYLUS)
event.getSource() == InputDevice.SOURCE_STYLUS)
{
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex);
TouchContext context = getTouchContext(actionIndex);
if (context == null) {
return super.onTouchEvent(event);
return false;
}
switch (event.getActionMasked())
{
case MotionEvent.ACTION_POINTER_DOWN:
@@ -441,12 +516,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE is special because it always has actionIndex == 0
// We'll call the move handlers for all indexes manually
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i].touchMoveEvent(eventX, eventY);
}
for (TouchContext aTouchContextMap : touchContextMap) {
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
aTouchContextMap.touchMoveEvent(
(int)event.getX(aTouchContextMap.getActionIndex()),
(int)event.getY(aTouchContextMap.getActionIndex()));
}
}
break;
default:
return super.onTouchEvent(event);
return false;
}
}
// This case is for mice
@@ -454,6 +534,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
int changedButtons = event.getButtonState() ^ lastButtonState;
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
@@ -462,7 +548,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
@@ -471,7 +557,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
@@ -480,48 +566,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
}
}
updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState();
}
else
{
return super.onTouchEvent(event);
// Unknown source
return false;
}
// Handled a known source
return true;
}
return super.onTouchEvent(event);
// Unknown class
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return handleMotionEvent(event) || super.onTouchEvent(event);
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (controllerHandler.handleMotionEvent(event)) {
return true;
}
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
{
switch (event.getActionMasked())
{
case MotionEvent.ACTION_HOVER_MOVE:
// Send a mouse move update (if neccessary)
updateMousePosition((int)event.getX(), (int)event.getY());
break;
case MotionEvent.ACTION_SCROLL:
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
break;
}
return true;
}
return super.onGenericMotionEvent(event);
}
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
}
private void updateMousePosition(int eventX, int eventY) {
// Send a mouse move if we already have a mouse location
@@ -535,8 +609,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Scale the deltas if the device resolution is different
// than the stream resolution
deltaX = (int)Math.round((double)deltaX * ((double)width / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * ((double)height / (double)screenSize.y));
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
@@ -548,15 +622,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
// Send it to the activity's motion event handler
return onGenericMotionEvent(event);
return handleMotionEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
// Send it to the activity's touch event handler
return onTouchEvent(event);
return handleMotionEvent(event);
}
@Override
@@ -569,6 +641,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void stageComplete(Stage stage) {
}
private void stopConnection() {
if (connecting || connected) {
connecting = connected = false;
conn.stop();
}
// Close the Evdev watcher to allow use of captured input devices
if (evdevWatcher != null) {
evdevWatcher.shutdown();
evdevWatcher = null;
}
}
@Override
public void stageFailed(Stage stage) {
@@ -579,9 +664,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (!displayedFailureDialog) {
displayedFailureDialog = true;
stopConnection();
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
conn.stop();
connecting = false;
}
}
@@ -590,9 +674,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (!displayedFailureDialog) {
displayedFailureDialog = true;
e.printStackTrace();
stopConnection();
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
conn.stop();
connected = false;
}
}
@@ -606,7 +690,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
connecting = false;
connected = true;
hideSystemUi();
hideSystemUi(1000);
}
@Override
@@ -621,7 +705,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void displayTransientMessage(final String message) {
if (!toastsDisabled) {
if (!prefConfig.disableWarnings) {
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -642,8 +726,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Resize the surface to match the aspect ratio of the video
// This must be done after the surface is created.
if (!stretchToFit && decoderRenderer.isHardwareAccelerated()) {
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), width, height);
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
prefConfig.width, prefConfig.height);
}
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
@@ -654,8 +739,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (connected) {
conn.stop();
connected = false;
stopConnection();
}
}
@@ -701,12 +785,39 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public void keyboardEvent(boolean buttonDown, short keyCode) {
short keyMap = keybTranslator.translate(keyCode);
if (keyMap != 0) {
if (handleSpecialKeys(keyMap, buttonDown)) {
return;
}
if (buttonDown) {
keybTranslator.sendKeyDown(keyMap, (byte) 0);
keybTranslator.sendKeyDown(keyMap, getModifierState());
}
else {
keybTranslator.sendKeyUp(keyMap, (byte) 0);
keybTranslator.sendKeyUp(keyMap, getModifierState());
}
}
}
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Don't do anything if we're not connected
if (!connected) {
return;
}
// This flag is set for all devices
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
hideSystemUi(2000);
}
// This flag is only set on 4.4+
else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
(visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
hideSystemUi(2000);
}
// This flag is only set before 4.4+
else if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT &&
(visibility & View.SYSTEM_UI_FLAG_LOW_PROFILE) == 0) {
hideSystemUi(2000);
}
}
}
@@ -6,6 +6,7 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
@@ -13,16 +14,20 @@ import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.root.R;
import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.StreamSettings;
import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
@@ -60,6 +65,9 @@ public class PcView extends Activity {
// Start updates
startComputerUpdates();
// Force a keypair to be generated early to avoid discovery delays
new AndroidCryptoProvider(PcView.this).getClientCertificate();
}
}.start();
}
@@ -69,59 +77,63 @@ public class PcView extends Activity {
}
};
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
}
private final static int APP_LIST_ID = 1;
private final static int PAIR_ID = 2;
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
private final static int DELETE_ID = 5;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
// Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
UiHelper.notifyNewRootView(this);
// Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view
settingsButton = (Button)findViewById(R.id.settingsButton);
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
pcList = (ListView)findViewById(R.id.pcListView);
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
pcListAdapter.setNotifyOnChange(false);
pcList.setAdapter(pcListAdapter);
pcList.setItemsCanFocus(true);
pcList.setOnItemClickListener(new OnItemClickListener() {
pcList.setAdapter(pcListAdapter);
pcList.setItemsCanFocus(true);
pcList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
if (computer.details == null) {
// Placeholder item; no context menu for it
return;
}
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
ComputerObject computer = pcListAdapter.getItem(pos);
if (computer.details == null) {
// Placeholder item; no context menu for it
return;
}
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
}
else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
}
else {
doAppList(computer.details);
}
}
else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
}
else {
doAppList(computer.details);
}
}
});
registerForContextMenu(pcList);
settingsButton.setOnClickListener(new OnClickListener() {
});
registerForContextMenu(pcList);
settingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(PcView.this, StreamSettings.class));
}
});
});
addComputerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
@@ -129,8 +141,27 @@ public class PcView extends Activity {
startActivity(i);
}
});
addListPlaceholder();
if (pcListAdapter.isEmpty()) {
addListPlaceholder();
}
else {
pcListAdapter.notifyDataSetChanged();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
pcListAdapter.setNotifyOnChange(false);
initializeViews();
}
private void startComputerUpdates() {
@@ -157,7 +188,7 @@ public class PcView extends Activity {
}
}
private void stopComputerUpdates() {
private void stopComputerUpdates(boolean wait) {
if (managerBinder != null) {
if (!runningPolling) {
return;
@@ -166,6 +197,11 @@ public class PcView extends Activity {
freezeUpdates = true;
managerBinder.stopPolling();
if (wait) {
managerBinder.waitForPollingStopped();
}
runningPolling = false;
}
}
@@ -190,7 +226,7 @@ public class PcView extends Activity {
protected void onPause() {
super.onPause();
stopComputerUpdates();
stopComputerUpdates(false);
}
@Override
@@ -202,13 +238,13 @@ public class PcView extends Activity {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
stopComputerUpdates();
stopComputerUpdates(false);
// Call superclass
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
ComputerObject computer = pcListAdapter.getItem(info.position);
if (computer == null || computer.details == null) {
startComputerUpdates();
return;
@@ -259,6 +295,9 @@ public class PcView extends Activity {
NvHTTP httpConn;
String message;
try {
// Stop updates and wait while pairing
stopComputerUpdates(true);
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
@@ -313,6 +352,9 @@ public class PcView extends Activity {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
// Start polling again
startComputerUpdates();
}
}).start();
}
@@ -443,7 +485,7 @@ public class PcView extends Activity {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
ComputerObject computer = pcListAdapter.getItem(info.position);
switch (item.getItemId())
{
case PAIR_ID:
@@ -0,0 +1,6 @@
package com.limelight;
/* This is a dummy class to allow for a separate icon
* and launcher for TV.
*/
public class PcViewTv extends PcView {}
@@ -23,15 +23,16 @@ import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
@@ -51,6 +52,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
private RSAPrivateKey key;
private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
@@ -71,7 +74,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
try {
FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()];
fin.read(fileData);
if (fin.read(fileData) != f.length()) {
// Failed to read
fileData = null;
}
fin.close();
return fileData;
} catch (IOException e) {
@@ -150,7 +156,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(name, serial, now, expirationDate, name, keyPair.getPublic());
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
@@ -177,7 +184,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
PEMWriter pemWriter = new PEMWriter(strWriter);
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
@@ -208,7 +215,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (this) {
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
@@ -235,7 +242,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (this) {
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
@@ -260,7 +267,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
}
public byte[] getPemEncodedClientCertificate() {
synchronized (this) {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
@@ -47,6 +47,22 @@ public class ControllerHandler {
public ControllerHandler(NvConnection conn) {
this.conn = conn;
// We want limelight-common to scale the axis values to match Xinput values
ControllerPacket.enableAxisScaling = true;
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
InputDevice.MotionRange range;
// First get the axis for SOURCE_JOYSTICK
range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK);
if (range == null) {
// Now try the axis for SOURCE_GAMEPAD
range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD);
}
return range;
}
private ControllerMapping createMappingForDevice(InputDevice dev) {
@@ -55,10 +71,10 @@ public class ControllerHandler {
mapping.leftStickXAxis = MotionEvent.AXIS_X;
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
InputDevice.MotionRange leftTriggerRange = dev.getMotionRange(MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = dev.getMotionRange(MotionEvent.AXIS_RTRIGGER);
InputDevice.MotionRange brakeRange = dev.getMotionRange(MotionEvent.AXIS_BRAKE);
InputDevice.MotionRange gasRange = dev.getMotionRange(MotionEvent.AXIS_GAS);
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
if (leftTriggerRange != null && rightTriggerRange != null)
{
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
@@ -73,8 +89,8 @@ public class ControllerHandler {
}
else
{
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null) {
String devName = dev.getName();
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
@@ -86,6 +102,7 @@ public class ControllerHandler {
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true;
mapping.isXboxController = true;
}
else {
// DS4 controller uses RX and RY for triggers
@@ -99,8 +116,8 @@ public class ControllerHandler {
}
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = dev.getMotionRange(MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = dev.getMotionRange(MotionEvent.AXIS_RZ);
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) {
@@ -108,8 +125,8 @@ public class ControllerHandler {
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
}
else {
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
// Try RX and RY now
if (rxRange != null && ryRange != null) {
@@ -120,8 +137,8 @@ public class ControllerHandler {
}
// Some devices have "hats" for d-pads
InputDevice.MotionRange hatXRange = dev.getMotionRange(MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = dev.getMotionRange(MotionEvent.AXIS_HAT_Y);
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
@@ -131,18 +148,54 @@ public class ControllerHandler {
}
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
InputDevice.MotionRange lsXRange = dev.getMotionRange(mapping.leftStickXAxis);
InputDevice.MotionRange lsYRange = dev.getMotionRange(mapping.leftStickYAxis);
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
if (lsXRange != null && lsYRange != null) {
mapping.leftStickDeadzoneRadius = Math.max(lsXRange.getFlat(), lsYRange.getFlat());
// The flat values should never be negative but we'll deal with it if they are
mapping.leftStickDeadzoneRadius = Math.max(Math.abs(lsXRange.getFlat()),
Math.abs(lsYRange.getFlat()));
// Some devices (certain OUYAs at least) report a deadzone that's larger
// than the entire range of their axis likely due to some system software bug.
// If we see a very large deadzone, simply ignore the value and use our default.
if (mapping.leftStickDeadzoneRadius > 0.5f) {
mapping.leftStickDeadzoneRadius = 0;
}
// If there isn't a (reasonable) deadzone at all, use 20%
if (mapping.leftStickDeadzoneRadius < 0.02f) {
mapping.leftStickDeadzoneRadius = 0.20f;
}
// Check that the deadzone is 15% at minimum
else if (mapping.leftStickDeadzoneRadius < 0.15f) {
mapping.leftStickDeadzoneRadius = 0.15f;
}
}
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
InputDevice.MotionRange rsXRange = dev.getMotionRange(mapping.rightStickXAxis);
InputDevice.MotionRange rsYRange = dev.getMotionRange(mapping.rightStickYAxis);
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
if (rsXRange != null && rsYRange != null) {
mapping.rightStickDeadzoneRadius = Math.max(rsXRange.getFlat(), rsYRange.getFlat());
// The flat values should never be negative but we'll deal with it if they are
mapping.rightStickDeadzoneRadius = Math.max(Math.abs(rsXRange.getFlat()),
Math.abs(rsYRange.getFlat()));
// Some devices (certain OUYAs at least) report a deadzone that's larger
// than the entire range of their axis likely due to some system software bug.
// If we see a very large deadzone, simply ignore the value and use our default.
if (mapping.rightStickDeadzoneRadius > 0.5f) {
mapping.rightStickDeadzoneRadius = 0;
}
// If there isn't a (reasonable) deadzone at all, use 20%
if (mapping.rightStickDeadzoneRadius < 0.02f) {
mapping.rightStickDeadzoneRadius = 0.20f;
}
// Check that the deadzone is 15% at minimum
else if (mapping.rightStickDeadzoneRadius < 0.15f) {
mapping.rightStickDeadzoneRadius = 0.15f;
}
}
}
@@ -175,9 +228,9 @@ public class ControllerHandler {
leftStickX, leftStickY, rightStickX, rightStickY);
}
private static int handleRemapping(ControllerMapping mapping, int keyCode) {
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
if (mapping.isDualShock4) {
switch (keyCode) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1;
@@ -216,7 +269,7 @@ public class ControllerHandler {
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
switch (keyCode) {
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
@@ -226,8 +279,26 @@ public class ControllerHandler {
return 0;
}
}
else if (mapping.hatXAxis == -1 &&
mapping.hatYAxis == -1 &&
mapping.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes
switch (event.getScanCode())
{
case 704:
return KeyEvent.KEYCODE_DPAD_LEFT;
case 705:
return KeyEvent.KEYCODE_DPAD_RIGHT;
case 706:
return KeyEvent.KEYCODE_DPAD_UP;
case 707:
return KeyEvent.KEYCODE_DPAD_DOWN;
}
}
return keyCode;
return event.getKeyCode();
}
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) {
@@ -242,7 +313,7 @@ public class ControllerHandler {
// Scale the input based on the distance from the deadzone
inputVector.getNormalized(normalizedInputVector);
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
// Bound the X value to -1.0 to 1.0
if (normalizedInputVector.getX() > 1.0f) {
normalizedInputVector.setX(1.0f);
@@ -284,7 +355,7 @@ public class ControllerHandler {
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
}
// Handle controllers with analog triggers
@@ -333,7 +404,7 @@ public class ControllerHandler {
return false;
}
keyCode = handleRemapping(mapping, keyCode);
keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
@@ -347,7 +418,7 @@ public class ControllerHandler {
// UI thread.
try {
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
} catch (InterruptedException e) {}
} catch (InterruptedException ignored) {}
}
switch (keyCode) {
@@ -424,7 +495,7 @@ public class ControllerHandler {
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
} catch (InterruptedException e) {}
} catch (InterruptedException ignored) {}
}
}
@@ -442,7 +513,7 @@ public class ControllerHandler {
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
} catch (InterruptedException e) {}
} catch (InterruptedException ignored) {}
}
}
@@ -456,7 +527,7 @@ public class ControllerHandler {
return false;
}
keyCode = handleRemapping(mapping, keyCode);
keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
@@ -566,5 +637,6 @@ public class ControllerHandler {
public float hatYDeadzone;
public boolean isDualShock4;
public boolean isXboxController;
}
}
@@ -21,6 +21,11 @@ public class TouchContext {
this.conn = conn;
this.actionIndex = actionIndex;
}
public int getActionIndex()
{
return actionIndex;
}
private boolean isTap()
{
@@ -65,7 +70,7 @@ public class TouchContext {
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
@@ -84,10 +89,8 @@ public class TouchContext {
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}
return false;
return true;
}
}
@@ -158,7 +158,7 @@ public class EvdevHandler {
try {
handlerThread.join();
} catch (InterruptedException e) {}
} catch (InterruptedException ignored) {}
}
public void notifyDeleted() {
@@ -3,6 +3,7 @@ package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Locale;
import com.limelight.LimeLog;
@@ -12,7 +13,7 @@ public class EvdevReader {
}
// Requires root to chmod /dev/input/eventX
public static boolean setPermissions(String[] files, int octalPermissions) {
public static boolean setPermissions(String[] files, int octalPermissions) {
ProcessBuilder builder = new ProcessBuilder("su");
try {
@@ -20,13 +21,14 @@ public class EvdevReader {
OutputStream stdin = p.getOutputStream();
for (String file : files) {
stdin.write(String.format("chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
}
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
@@ -2,18 +2,21 @@ package com.limelight.binding.input.evdev;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import com.limelight.LimeLog;
import android.os.FileObserver;
@SuppressWarnings("ALL")
public class EvdevWatcher {
private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event";
private HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false;
private boolean init = false;
private boolean ungrabbed = false;
private EvdevListener listener;
private Thread startThread;
@@ -42,7 +45,11 @@ public class EvdevWatcher {
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
handler.start();
// If we're ungrabbed now, don't start the handler
if (!ungrabbed) {
handler.start();
}
handlers.put(fileName, handler);
}
@@ -67,6 +74,9 @@ public class EvdevWatcher {
// Rundown existing files
File devInputDir = new File(PATH);
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
// Set desired permissions
String[] filePaths = new String[files.length];
@@ -78,6 +88,31 @@ public class EvdevWatcher {
return files;
}
public void ungrabAll() {
synchronized (handlers) {
// Note that we're ungrabbed for now
ungrabbed = true;
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
public void regrabAll() {
synchronized (handlers) {
// We're regrabbing everything now
ungrabbed = false;
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
// We need to recreate each entry since we can't reuse a stopped one
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
entry.getValue().start();
}
}
}
public void start() {
startThread = new Thread() {
@Override
@@ -119,10 +154,15 @@ public class EvdevWatcher {
// Stop the observer
observer.stopWatching();
synchronized (handlers) {
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();
@@ -18,6 +18,7 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread;
@@ -38,6 +39,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null;
@@ -93,7 +95,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = findOptimalPerformanceLevel();
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
int avcFlags = 0;
@@ -25,7 +25,7 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecDecoderRenderer.findProbableSafeDecoder() != null)) {
MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
}
else {
@@ -0,0 +1,540 @@
package com.limelight.binding.video;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.locks.LockSupport;
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.os.Build;
import android.view.SurfaceHolder;
@SuppressWarnings("unused")
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private boolean adaptivePlayback;
private int initialWidth, initialHeight;
private long lastTimestampUs;
private long totalTimeMs;
private long decoderTimeMs;
private int totalFrames;
private String decoderName;
private int numSpsIn;
private int numPpsIn;
private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() {
//dumpDecoders();
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
if (decoder == null) {
decoder = MediaCodecHelper.findFirstDecoder();
}
if (decoder == null) {
// This case is handled later in setup()
return;
}
decoderName = decoder.getName();
// Set decoder-specific attributes
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
}
isExynos4 = MediaCodecHelper.isExynos4Device();
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
this.initialHeight = height;
if (decoderName == null) {
LimeLog.severe("No available hardware decoder!");
return false;
}
// Codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems
try {
videoDecoder = MediaCodec.createByCodecName(decoderName);
} catch (Exception e) {
return false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width);
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) {
try {
// FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true);
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) {
// What do we do here?
e.printStackTrace();
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace();
}
else {
LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace();
}
}
});
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
LimeLog.info("Using hardware decoding");
return true;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
if (codecExc.isTransient()) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return;
}
LimeLog.severe(codecExc.getDiagnosticInfo());
}
}
if (buf != null || codecFlags != 0) {
throw new RendererException(dr, e, buf, codecFlags);
}
else {
throw new RendererException(dr, e);
}
}
private void startRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@Override
public void run() {
BufferInfo info = new BufferInfo();
DecodeUnit du = null;
int inputIndex = -1;
while (!isInterrupted())
{
// 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.
if (inputIndex == -1 && du == null) {
try {
for (int i = 0; i < 5; i++) {
inputIndex = videoDecoder.dequeueInputBuffer(0);
du = depacketizer.pollNextDecodeUnit();
// Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) {
break;
}
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
du = null;
inputIndex = -1;
}
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
// Grab an input buffer if we don't have one already.
// This way we can have one ready hopefully by the time
// the depacketizer is done with this frame. It's important
// that this can timeout because it's possible that we could exhaust
// the decoder's input buffers and deadlocks because aren't pulling
// frames out of the other end.
if (inputIndex == -1) {
try {
// If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately.
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
// Grab a decode unit if we don't have one already
if (du == null) {
du = depacketizer.pollNextDecodeUnit();
}
// If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) {
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
// DU and input buffer have both been consumed
du = null;
inputIndex = -1;
}
// Try to output a frame
try {
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta > 5 && delta < 300) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
// Getting an input buffer may already block
// so don't park if we still need to do that
if (inputIndex >= 0) {
LockSupport.parkNanos(1);
}
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
}
@SuppressWarnings("deprecation")
@Override
public boolean start(VideoDepacketizer depacketizer) {
this.depacketizer = depacketizer;
// Start the decoder
videoDecoder.start();
// On devices pre-Lollipop, we'll use a rendering thread
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread();
}
return true;
}
@Override
public void stop() {
if (rendererThread != null) {
// Halt the rendering thread
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException ignored) { }
}
// Stop the decoder
videoDecoder.stop();
}
@Override
public void release() {
if (videoDecoder != null) {
videoDecoder.release();
}
}
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
int i;
Exception lastException = null;
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
0, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(this, e, null, codecFlags);
lastException = e;
}
}
if (i == 25) {
throw new RendererException(this, lastException, null, codecFlags);
}
}
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
long timestampUs = currentTime * 1000;
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
timestampUs = lastTimestampUs + 1;
}
lastTimestampUs = timestampUs;
// Clear old input data
buf.clear();
int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags();
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
numIframeIn++;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
ByteBuffer spsBuf = ByteBuffer.wrap(header.data);
// Skip to the start of the NALU data
spsBuf.position(header.offset+5);
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// also requires this fixup.
//
// 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;
if (needsSpsBitstreamFixup || isExynos4) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra.
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
}
// Write the annex B header
buf.put(header.data, header.offset, 5);
// Write the modified SPS to the input buffer
sps.write(buf);
queueInputBuffer(inputBufferIndex,
0, buf.position(),
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
return;
} else if (header.data[header.offset+4] == 0x68) {
numPpsIn++;
}
}
// Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
{
buf.put(desc.data, desc.offset, desc.length);
}
queueInputBuffer(inputBufferIndex,
0, decodeUnit.getDataLength(),
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
}
@Override
public int getCapabilities() {
return adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
}
@Override
public int getAverageDecoderLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFrames);
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException;
private MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.renderer = renderer;
this.originalException = e;
}
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.renderer = renderer;
this.originalException = e;
this.currentBuffer = currentBuffer;
this.currentCodecFlags = currentCodecFlags;
}
public String toString() {
String str = "";
str += "Decoder: "+renderer.decoderName+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
if (currentBuffer != null) {
str += "Current buffer: ";
currentBuffer.flip();
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
str += String.format((Locale)null, "%02x ", currentBuffer.get());
}
str += "\n";
str += "Buffer codec flags: "+currentCodecFlags+"\n";
}
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
} catch (Exception e) {
str += e.getMessage();
}
str += "Full decoder dump:\n";
try {
str += MediaCodecHelper.dumpDecoders();
} catch (Exception e) {
str += e.getMessage();
}
str += originalException.toString();
return str;
}
}
}
@@ -0,0 +1,300 @@
package com.limelight.binding.video;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.os.Build;
import com.limelight.LimeLog;
public class MediaCodecHelper {
public static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
static {
preferredDecoders = new LinkedList<String>();
}
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
static {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
if (prefix.equalsIgnoreCase(badPrefix)) {
return true;
}
}
}
return false;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
}
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
{
// This will make getCapabilities() return that adaptive playback is supported
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
return true;
}
} catch (Exception e) {
// Tolerate buggy codecs
}
}
return false;
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
Collections.addAll(infoList, mcl.getCodecInfos());
}
else {
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
infoList.add(MediaCodecList.getCodecInfoAt(i));
}
}
return infoList;
}
@SuppressWarnings("RedundantThrows")
public static String dumpDecoders() throws Exception {
String str = "";
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
str += "Decoder: "+codecInfo.getName()+"\n";
for (String type : codecInfo.getSupportedTypes()) {
str += "\t"+type+"\n";
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
for (CodecProfileLevel profile : caps.profileLevels) {
str += "\t\t"+profile.profile+" "+profile.level+"\n";
}
}
}
return str;
}
public static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
for (String preferredDecoder : preferredDecoders) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for preferred decoders
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
return codecInfo;
}
}
}
return null;
}
public static MediaCodecInfo findFirstDecoder() {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
}
}
return null;
}
public static MediaCodecInfo findProbableSafeDecoder() {
// First look for a preferred decoder by name
MediaCodecInfo info = findPreferredDecoder();
if (info != null) {
return info;
}
// Now look for decoders we know are safe
try {
// If this function completes, it will determine if the decoder is safe
return findKnownSafeDecoder();
} catch (Exception e) {
// Some buggy devices seem to throw exceptions
// from getCapabilitiesForType() so we'll just assume
// they're okay and go with the first one we find
return findFirstDecoder();
}
}
// We declare this method as explicitly throwing Exception
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264 high profile
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
}
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}
return null;
}
public static String readCpuinfo() throws Exception {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
try {
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
return cpuInfo.toString();
} finally {
br.close();
}
}
private static boolean stringContainsIgnoreCase(String string, String substring) {
return string.toLowerCase(Locale.ENGLISH).contains(substring.toLowerCase(Locale.ENGLISH));
}
public static boolean isExynos4Device() {
try {
// Try reading CPU info too look for
String cpuInfo = readCpuinfo();
// SMDK4xxx is Exynos 4
if (stringContainsIgnoreCase(cpuInfo, "SMDK4")) {
LimeLog.info("Found SMDK4 in /proc/cpuinfo");
return true;
}
// If we see "Exynos 4" also we'll count it
if (stringContainsIgnoreCase(cpuInfo, "Exynos 4")) {
LimeLog.info("Found Exynos 4 in /proc/cpuinfo");
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
File systemDir = new File("/sys/devices/system");
File[] files = systemDir.listFiles();
if (files != null) {
for (File f : files) {
if (stringContainsIgnoreCase(f.getName(), "exynos4")) {
LimeLog.info("Found exynos4 in /sys/devices/system");
return true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
@@ -4,6 +4,7 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.LimeLog;
@@ -44,7 +45,7 @@ public class ComputerDatabaseManager {
private void initializeDb() {
// Create tables if they aren't already there
computerDb.execSQL(String.format("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)",
COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
@@ -1,11 +1,7 @@
package com.limelight.computers;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
@@ -24,16 +20,12 @@ import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.IBinder;
public class ComputerManagerService extends Service {
private static final int MAX_CONCURRENT_REQUESTS = 4;
private static final int POLLING_PERIOD_MS = 5000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
@@ -43,12 +35,12 @@ public class ComputerManagerService extends Service {
private AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private ThreadPoolExecutor pollingPool;
private Timer pollingTimer;
private HashMap<ComputerDetails, Thread> pollingThreads;
private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0);
private DiscoveryService.DiscoveryBinder discoveryBinder;
private ServiceConnection discoveryServiceConnection = new ServiceConnection() {
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
synchronized (discoveryServiceConnection) {
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
@@ -66,6 +58,79 @@ public class ComputerManagerService extends Service {
discoveryBinder = null;
}
};
// Returns true if the details object was modified
private boolean runPoll(ComputerDetails details)
{
boolean newPc = (details.name == null);
if (!getLocalDatabaseReference()) {
return false;
}
activePolls.incrementAndGet();
// Poll the machine
if (!doPollMachine(details)) {
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
activePolls.decrementAndGet();
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
if (!newPc) {
// Check if it's in the database because it could have been
// removed after this was issued
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return true;
}
}
dbManager.updateComputer(details);
}
// Don't call the listener if this is a failed lookup of a new PC
if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) {
listener.notifyComputerUpdated(details);
}
releaseLocalDatabaseReference();
return true;
}
private Thread createPollingThread(final ComputerDetails details) {
Thread t = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
ComputerDetails originalDetails = new ComputerDetails();
originalDetails.update(details);
// Check if this poll has modified the details
if (runPoll(details) && !originalDetails.equals(details)) {
// Replace our thread entry with the new one
synchronized (pollingThreads) {
pollingThreads.remove(originalDetails);
pollingThreads.put(details, this);
}
}
// Wait until the next polling interval
try {
Thread.sleep(POLLING_PERIOD_MS);
} catch (InterruptedException e) {
break;
}
}
}
};
t.setName("Polling thread for "+details.localIp.getHostAddress());
return t;
}
public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) {
@@ -76,8 +141,22 @@ public class ComputerManagerService extends Service {
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
// Start polling known machines
pollingTimer = new Timer();
pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS);
if (!getLocalDatabaseReference()) {
return;
}
List<ComputerDetails> computerList = dbManager.getAllComputers();
releaseLocalDatabaseReference();
synchronized (pollingThreads) {
for (ComputerDetails computer : computerList) {
// This polling thread might already be there
if (!pollingThreads.containsKey(computer)) {
Thread t = createPollingThread(computer);
pollingThreads.put(computer, t);
t.start();
}
}
}
}
public void waitForReady() {
@@ -87,11 +166,19 @@ public class ComputerManagerService extends Service {
// Wait for the bind notification
discoveryServiceConnection.wait(1000);
}
} catch (InterruptedException e) {
} catch (InterruptedException ignored) {
}
}
}
public void waitForPollingStopped() {
while (activePolls.get() != 0) {
try {
Thread.sleep(250);
} catch (InterruptedException ignored) {}
}
}
public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr);
}
@@ -120,10 +207,12 @@ public class ComputerManagerService extends Service {
discoveryBinder.stopDiscovery();
// Stop polling
if (pollingTimer != null) {
pollingTimer.cancel();
pollingTimer = null;
}
synchronized (pollingThreads) {
for (Thread t : pollingThreads.values()) {
t.interrupt();
}
pollingThreads.clear();
}
// Remove the listener
listener = null;
@@ -151,17 +240,24 @@ public class ComputerManagerService extends Service {
}
};
}
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Put it in the thread pool to process later
pollingPool.execute(getPollingRunnable(fakeDetails));
// Spawn a thread for this computer
synchronized (pollingThreads) {
// This polling thread might already be there
if (!pollingThreads.containsKey(fakeDetails)) {
Thread t = createPollingThread(fakeDetails);
pollingThreads.put(fakeDetails, t);
t.start();
}
}
}
public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
@@ -169,7 +265,7 @@ public class ComputerManagerService extends Service {
fakeDetails.remoteIp = addr;
// Block while we try to fill the details
getPollingRunnable(fakeDetails).run();
runPoll(fakeDetails);
// If the machine is reachable, it was successful
return fakeDetails.state == ComputerDetails.State.ONLINE;
@@ -201,115 +297,11 @@ public class ComputerManagerService extends Service {
}
}
private TimerTask getTimerTask() {
return new TimerTask() {
@Override
public void run() {
if (!getLocalDatabaseReference()) {
return;
}
List<ComputerDetails> computerList = dbManager.getAllComputers();
releaseLocalDatabaseReference();
for (ComputerDetails computer : computerList) {
pollingPool.execute(getPollingRunnable(computer));
}
}
};
}
private int getActiveNetworkType() {
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
if (activeNetworkInfo == null) {
return -1;
}
return activeNetworkInfo.getType();
}
private InterfaceAddress getActiveInterfaceAddress() {
String matchingPrefix;
switch (getActiveNetworkType())
{
case ConnectivityManager.TYPE_ETHERNET:
matchingPrefix = "eth";
break;
case ConnectivityManager.TYPE_WIFI:
matchingPrefix = "wlan";
break;
default:
// Must be on Ethernet or Wifi to consider that we can send large packets
return null;
}
// Try to find the interface that corresponds to the active network
try {
Enumeration<NetworkInterface> ifaceList = NetworkInterface.getNetworkInterfaces();
while (ifaceList.hasMoreElements()) {
NetworkInterface iface = ifaceList.nextElement();
// Look for an interface that matches the prefix we expect
if (iface.isUp() && iface.getName().startsWith(matchingPrefix)) {
// Find the IPv4 address for the interface
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
if (!(addr.getAddress() instanceof Inet4Address)) {
// Skip non-IPv4 addresses
continue;
}
// Found the right address on the right interface
return addr;
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
// We didn't find the interface or something else went wrong
return null;
}
private boolean isOnSameSubnet(InetAddress targetAddress, InetAddress localAddress, short networkPrefixLength) {
byte[] targetBytes = targetAddress.getAddress();
byte[] localBytes = localAddress.getAddress();
for (int byteIndex = 0; networkPrefixLength > 0; byteIndex++) {
byte target = targetBytes[byteIndex];
byte local = localBytes[byteIndex];
if (networkPrefixLength >= 8) {
// Do a full byte comparison
if (target != local) {
return false;
}
networkPrefixLength -= 8;
}
else {
target &= (byte)(0xFF << (8 - networkPrefixLength));
local &= (byte)(0xFF << (8 - networkPrefixLength));
// Do a masked comparison
if (target != local) {
return false;
}
networkPrefixLength = 0;
}
}
return true;
}
private ComputerDetails tryPollIp(InetAddress ipAddr) {
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
return http.getComputerDetails();
} catch (Exception e) {
return null;
@@ -326,7 +318,7 @@ public class ComputerManagerService extends Service {
polledDetails = tryPollIp(details.remoteIp);
}
if (polledDetails == null) {
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details.localIp);
@@ -340,7 +332,8 @@ public class ComputerManagerService extends Service {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
} else {
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
@@ -356,68 +349,7 @@ public class ComputerManagerService extends Service {
}
private boolean doPollMachine(ComputerDetails details) {
// Get the network type
int networkType = getActiveNetworkType();
switch (networkType) {
// We'll check local first on these if we find
// we're on the same subnet
case ConnectivityManager.TYPE_ETHERNET:
case ConnectivityManager.TYPE_WIFI:
InterfaceAddress ifaceAddr = getActiveInterfaceAddress();
if (ifaceAddr != null) {
if (isOnSameSubnet(details.localIp, ifaceAddr.getAddress(), ifaceAddr.getNetworkPrefixLength())) {
// It's on the same subnet, so poll local first
LimeLog.info("Machine looks local; trying local IP first");
return pollComputer(details, true);
}
}
// Fall through to remote first
default:
LimeLog.info("Machine looks remote; trying remote IP first");
return pollComputer(details, false);
}
}
private Runnable getPollingRunnable(final ComputerDetails details) {
return new Runnable() {
@Override
public void run() {
boolean newPc = (details.name == null);
if (!getLocalDatabaseReference()) {
return;
}
// Poll the machine
if (!doPollMachine(details)) {
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
if (!newPc) {
// Check if it's in the database because it could have been
// removed after this was issued
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return;
}
}
dbManager.updateComputer(details);
}
// Don't call the listener if this is a failed lookup of a new PC
if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) {
listener.notifyComputerUpdated(details);
}
releaseLocalDatabaseReference();
}
};
return pollComputer(details, true);
}
@Override
@@ -425,11 +357,8 @@ public class ComputerManagerService extends Service {
// Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
// Create the thread pool for updating computer state
pollingPool = new ThreadPoolExecutor(MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_REQUESTS,
Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.DiscardPolicy());
pollingThreads = new HashMap<ComputerDetails, Thread>();
// Lookup or generate this device's UID
idManager = new IdentityManager(this);
@@ -446,9 +375,6 @@ public class ComputerManagerService extends Service {
unbindService(discoveryServiceConnection);
}
// Stop the thread pool
pollingPool.shutdownNow();
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
// Remove the initial DB reference
@@ -4,6 +4,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Locale;
import java.util.Random;
import com.limelight.LimeLog;
@@ -53,7 +54,7 @@ public class IdentityManager {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {}
} catch (IOException ignored) {}
}
}
}
@@ -61,7 +62,7 @@ public class IdentityManager {
private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string
LimeLog.info("Generating new UID");
String uidStr = String.format("%016x", new Random().nextLong());
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
@@ -75,7 +76,7 @@ public class IdentityManager {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {}
} catch (IOException ignored) {}
}
}
@@ -1,12 +1,13 @@
package com.limelight;
package com.limelight.preferences;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.root.R;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
@@ -98,7 +99,7 @@ public class AddComputerManually extends Activity {
try {
addThread.join();
} catch (InterruptedException e) {}
} catch (InterruptedException ignored) {}
addThread = null;
}
@@ -126,8 +127,10 @@ public class AddComputerManually extends Activity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_computer_manually);
this.addPcButton = (Button) findViewById(R.id.addPc);
UiHelper.notifyNewRootView(this);
this.addPcButton = (Button) findViewById(R.id.addPc);
this.hostText = (TextView) findViewById(R.id.hostTextView);
// Bind to the ComputerManager service
@@ -0,0 +1,141 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String DECODER_PREF_STRING = "list_decoders";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 30;
private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
public int width, height, fps;
public int bitrate;
public int decoder;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (resFpsString.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (resFpsString.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (str.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (str.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (str.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
}
private static int getDecoderValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
if (str.equals("auto")) {
return AUTOSELECT_DECODER;
}
else if (str.equals("software")) {
return FORCE_SOFTWARE_DECODER;
}
else if (str.equals("hardware")) {
return FORCE_HARDWARE_DECODER;
}
else {
// Should never get here
return AUTOSELECT_DECODER;
}
}
public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
}
else if (str.equals("720p60")) {
config.width = 1280;
config.height = 720;
config.fps = 60;
}
else if (str.equals("1080p30")) {
config.width = 1920;
config.height = 1080;
config.fps = 30;
}
else if (str.equals("1080p60")) {
config.width = 1920;
config.height = 1080;
config.fps = 60;
}
else {
// Should never get here
config.width = 1280;
config.height = 720;
config.fps = 60;
}
config.decoder = getDecoderValue(context);
// Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
return config;
}
}
@@ -0,0 +1,159 @@
package com.limelight.preferences;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
public class SeekBarPreference extends DialogPreference
{
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private SeekBar seekBar;
private TextView splashText, valueText;
private Context context;
private String dialogMessage, suffix;
private int defaultValue, maxValue, currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
// Read the message from XML
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
if (dialogMessageId == 0) {
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
}
else {
dialogMessage = context.getString(dialogMessageId);
}
// Get the suffix for the number displayed in the dialog
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
if (suffixId == 0) {
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
}
else {
suffix = context.getString(suffixId);
}
// Get default and max seekbar values
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
}
@Override
protected View onCreateDialogView() {
LinearLayout.LayoutParams params;
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6);
splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) {
splashText.setText(dialogMessage);
}
layout.addView(splashText);
valueText = new TextView(context);
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.FILL_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params);
seekBar = new SeekBar(context);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(" " + suffix));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue);
}
seekBar.setMax(maxValue);
seekBar.setProgress(currentValue);
return layout;
}
@Override
protected void onBindDialogView(View v) {
super.onBindDialogView(v);
seekBar.setMax(maxValue);
seekBar.setProgress(currentValue);
}
@Override
protected void onSetInitialValue(boolean restore, Object defaultValue)
{
super.onSetInitialValue(restore, defaultValue);
if (restore) {
currentValue = shouldPersist() ? getPersistedInt(this.defaultValue) : 0;
}
else {
currentValue = (Integer) defaultValue;
}
}
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) {
this.currentValue = progress;
if (seekBar != null) {
seekBar.setProgress(progress);
}
}
public int getProgress() {
return currentValue;
}
@Override
public void showDialog(Bundle state) {
super.showDialog(state);
Button positiveButton = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (shouldPersist()) {
currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress());
callChangeListener(Integer.valueOf(seekBar.getProgress()));
}
((AlertDialog) getDialog()).dismiss();
}
});
}
}
@@ -0,0 +1,54 @@
package com.limelight.preferences;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.app.Activity;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import com.limelight.R;
import com.limelight.utils.UiHelper;
public class StreamSettings extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
UiHelper.notifyNewRootView(this);
}
public static class SettingsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(valueStr))
.apply();
// Allow the original preference change to take place
return true;
}
});
}
}
}
@@ -13,7 +13,7 @@ public class Dialog implements Runnable {
private AlertDialog alert;
private static 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)
{
@@ -57,7 +57,7 @@ public class Dialog implements Runnable {
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
@@ -14,7 +14,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
private ProgressDialog progress;
private boolean finish;
private static 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)
{
@@ -0,0 +1,31 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import android.view.View;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void notifyNewRootView(Activity activity)
{
View rootView = (View) activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
}
}
@@ -8,5 +8,6 @@ LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
@@ -7,6 +7,9 @@
#include <linux/input.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>
#include <android/log.h>
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
@@ -67,6 +70,8 @@ Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject thi
data = (*env)->GetByteArrayElements(env, buffer, NULL);
if (data == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Failed to get byte array");
return -1;
}
@@ -83,10 +88,23 @@ Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject thi
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = read(fd, data, sizeof(struct input_event));
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() failed: %d", errno);
}
}
else {
// There must have been a failure
ret = -1;
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"poll() failed: %d", errno);
}
else {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Unexpected revents: %d", pollinfo.revents);
}
}
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);

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