Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8c4df4897 | |||
| 5ee16124bc | |||
| 8702ac72f0 | |||
| 004552ec30 | |||
| 2f28400234 | |||
| 78d213d686 | |||
| 1a71dda243 | |||
| 21822f259c | |||
| 4f79607015 | |||
| d8576d4c50 | |||
| 2f4042da8f | |||
| c1397e331b | |||
| cd182b3265 | |||
| 28f2d7b84a | |||
| e8de7908fd | |||
| 419c4c5592 | |||
| a9a8346f58 | |||
| 7e1b3f861f | |||
| f4204e1268 | |||
| 60f35cd0aa | |||
| bbcdaa94a1 | |||
| 8f6e8c00ef | |||
| 24cb347b10 | |||
| d1b4e9464f | |||
| 18f7bfab7f | |||
| d84b4bcf9a | |||
| 57d919798a | |||
| efeeebb0a2 | |||
| bc1409ba6c | |||
| 6f05b2af8a | |||
| d441bef33e | |||
| 7b1f6ee483 | |||
| 332960922a | |||
| ac03f73cf9 | |||
| fa847ef2fc | |||
| b19360ac75 | |||
| 562569dc6b | |||
| a18aa51f5a | |||
| b492ac43f8 | |||
| 5bf3efb247 | |||
| 2d833c32b0 | |||
| 19bade01b8 | |||
| 1b991ba432 | |||
| 9c48850bb7 | |||
| 3e6f5ff11c | |||
| 57c3d8af8b | |||
| 8530451c8b | |||
| 69a5c0b5b3 | |||
| a7c36dcde6 | |||
| dff6fc21f4 | |||
| 895e0250d9 | |||
| fd538cbaff | |||
| 27ce6fa203 | |||
| 8efe194682 | |||
| 947882d16f | |||
| f07e927103 | |||
| a61b85b494 | |||
| 0c4a049a80 | |||
| 8403101d0f | |||
| 47d47afd73 | |||
| 1430801888 | |||
| 6efc7e254b | |||
| ace1339811 | |||
| a5171a1701 | |||
| a50211ab95 | |||
| 31677adaa0 | |||
| 247a19766c | |||
| 1f09cbd609 | |||
| 731e4dc31e | |||
| b6ee0764ff | |||
| cd4bf9a28b | |||
| f56b7ff79e | |||
| 645ea683ee | |||
| 67e22fca6b | |||
| a726ba8ea7 | |||
| 23fcaa1bab | |||
| ad684a6f6b | |||
| d3438f4938 | |||
| cafdc21bf2 | |||
| ceb9bd3342 | |||
| 196c0e6cbc | |||
| e2cb7c953c | |||
| 426b3c8522 | |||
| 9648cf257f | |||
| 31d8687f67 | |||
| 991407a2cf | |||
| 13b80eda8a | |||
| 6677949614 | |||
| 080dcd92d7 | |||
| 31b0bcf041 | |||
| 36664133f8 | |||
| a3106bffca | |||
| 94a26fb831 | |||
| 1b026f1354 | |||
| 0517e8a530 | |||
| a9fea34ac1 | |||
| 201704dc9d | |||
| 62ecb1af50 | |||
| 9d4ca6293f | |||
| 2296b80edb | |||
| 5577d48dcf | |||
| e92a281fd8 | |||
| b4c3f9678a | |||
| 82f79c466a | |||
| d428f316b4 | |||
| 828f4877b6 | |||
| 09e8e8e6b3 | |||
| 77c8051ec6 | |||
| 819c5e823c | |||
| 6bae056e3a | |||
| bb869a51fd | |||
| 25b3d08bb9 | |||
| 66eb890462 | |||
| cde8ec8262 | |||
| ef1429a639 | |||
| 85a011eb84 | |||
| b5e585834d | |||
| ae298fbc51 | |||
| c02e1ed006 | |||
| 178c53ee84 | |||
| 2c23dbd2be | |||
| 3e017625a9 | |||
| 124037ce27 | |||
| bc166a713d | |||
| 364a9fa7d7 | |||
| f4546ba188 | |||
| 5de2a8f6ec | |||
| 2365cd2978 | |||
| e8dd3511db | |||
| ae40a9736a | |||
| 08cc93d337 | |||
| 5fdd9b773c | |||
| e3e7ac1e68 | |||
| 10212bd38b | |||
| 4bec02f47f | |||
| 79f888fe47 | |||
| 9393bf7f79 | |||
| 1b1d4399a9 | |||
| b2ad259a7c | |||
| ac5c264090 | |||
| fcfdd4e323 | |||
| ea65bb2c0a | |||
| b340055588 | |||
| 2918039b6f | |||
| e8fd1f262a | |||
| 8887401644 | |||
| f892db6ee8 | |||
| 829532c572 | |||
| 875eb1e773 | |||
| f295289774 | |||
| 1f69b4f271 | |||
| 20a5a844db | |||
| 1a7a2f848e | |||
| 887dd9aa21 | |||
| e1e4ccf318 | |||
| ee6edd2404 | |||
| f0f801ba3f | |||
| 6a7e06b3d5 | |||
| 48ab233d0f | |||
| b9f01b63cc | |||
| 428d37afd4 | |||
| f5ec665115 | |||
| 92143df65c | |||
| d428f342f7 | |||
| e2663f06ba | |||
| 9cf592ee26 | |||
| 4258ac752e | |||
| 8ec8fff57c | |||
| cf15b79e35 | |||
| 007d488201 | |||
| 5beffed9b9 | |||
| 532ec6f8f3 | |||
| a46fb7ba36 | |||
| bc2f2de6c2 | |||
| efec35afa6 | |||
| 993711ccfe | |||
| d1ef912984 | |||
| d2f8ee8b81 | |||
| 91e68c0580 | |||
| a591dcec48 | |||
| 1898fcd741 | |||
| 8bc8f14c64 | |||
| 09cf5d23ea | |||
| bec5cfe4a6 | |||
| 59f7595050 | |||
| 9e20d25093 | |||
| 155432b4b8 | |||
| 0c614516fb | |||
| 1734d21b8e | |||
| 825233309d | |||
| 0f4e5a4585 | |||
| 9be149942a | |||
| 2ab14cdbf5 | |||
| b79fd8860c | |||
| 6f6eb1fb95 | |||
| c50ec21f39 | |||
| 3176ee72fe | |||
| cc37da9a56 | |||
| 87572bb083 | |||
| a74de39879 | |||
| 1d27309e53 | |||
| 3f00885d2c | |||
| ea3b1a7438 | |||
| 3dd57e9f38 | |||
| a639143e94 | |||
| b3c78ce1b1 | |||
| ddf9284ece | |||
| 438be4b8e5 | |||
| 8f43b95129 | |||
| faa2be431f | |||
| 16de523e78 | |||
| b24abc6ddd | |||
| a450cd5b01 | |||
| 2e3b7a2c09 | |||
| 22bba877d7 | |||
| 7142db3fac | |||
| 37cf572c0c | |||
| 33c5254d6f | |||
| faa82ca9d6 | |||
| 0d35ea5207 | |||
| 579645c07c | |||
| 869cbe2e81 | |||
| 329a938bf8 | |||
| 411931cc27 | |||
| ce01223683 | |||
| e7501a488d | |||
| 5626e9663b | |||
| 01b35ccdd3 | |||
| e83bc747c8 | |||
| cbe4af7623 | |||
| fc9e45270a | |||
| 94c1fc2b66 | |||
| 49999634c1 | |||
| 09f4827d02 | |||
| 52e4e81e35 | |||
| 56b752f63f | |||
| 2e6e835a8e | |||
| d8b0a0ffb5 | |||
| b82d74474a | |||
| 508b855e36 |
-10
@@ -1,10 +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="lib" path="libs/limelight-common.jar"/>
|
|
||||||
<classpathentry kind="output" path="bin/classes"/>
|
|
||||||
</classpath>
|
|
||||||
+35
-1
@@ -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
|
||||||
@@ -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 +0,0 @@
|
|||||||
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
-3
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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="<?xml version="1.0" encoding="UTF-8"?>
<packageExplorer group_libraries="1" layout="2" linkWithEditor="0" rootMode="1" workingSetName="">
<customFilters userDefinedPatternsEnabled="false">
<xmlDefinedFilters>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.StaticsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.FieldsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter" isEnabled="true"/>
</xmlDefinedFilters>
</customFilters>
</packageExplorer>" 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 +0,0 @@
|
|||||||
org.eclipse.core.runtime=1
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.limelight"
|
|
||||||
android:versionCode="13"
|
|
||||||
android:versionName="2.2" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="16"
|
|
||||||
android:targetSdkVersion="19" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/AppTheme" >
|
|
||||||
<activity
|
|
||||||
android:name="com.limelight.Connection"
|
|
||||||
android:label="@string/app_name" >
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="com.limelight.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" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="com.limelight.Connection" />
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,45 +1,39 @@
|
|||||||
#Limelight
|
#Limelight
|
||||||
|
|
||||||
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software, and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Limelight will allow you to stream your full collection of Steam games from your Windows PC to your Android device,
|
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
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
|
##Features
|
||||||
|
|
||||||
* Streams Steam and all of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
##Features in development
|
|
||||||
|
|
||||||
* Use mDNS to scan for compatible GeForce Experience (GFE) machines on the network
|
|
||||||
* Choose from the list of available games instead of just launching Steam
|
|
||||||
* Keyboard input
|
|
||||||
|
|
||||||
##Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Limelight for Android from
|
* Download and install Limelight for Android from
|
||||||
[XDA](http://forum.xda-developers.com/showthread.php?t=2505510)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
##Requirements
|
##Requirements
|
||||||
|
|
||||||
* [GFE compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
* [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
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* 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
|
##Usage
|
||||||
|
|
||||||
* Turn on Shield Streaming in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* In Limelight, enter your PC's IP or Hostname and click "Pair"
|
* When on the same network as your PC, open Limelight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC
|
||||||
* In Limelight, click "Start Streaming"
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
##Contribute
|
##Contribute
|
||||||
|
|||||||
+112
@@ -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>
|
||||||
|
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.limelight">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
|
<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:theme="@style/AppTheme" >
|
||||||
|
|
||||||
|
<!-- Launcher for traditional devices -->
|
||||||
|
<activity
|
||||||
|
android:name=".PcView"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Launcher for Android TV devices -->
|
||||||
|
<activity
|
||||||
|
android:name=".PcViewTv"
|
||||||
|
android:logo="@drawable/atv_banner"
|
||||||
|
android:icon="@drawable/atv_banner"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AppView"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.PcView" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
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=".preferences.AddComputerManually"
|
||||||
|
android:label="Add Computer Manually" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.PcView" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".Game"
|
||||||
|
android:screenOrientation="sensorLandscape"
|
||||||
|
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=".discovery.DiscoveryService"
|
||||||
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
|
<service
|
||||||
|
android:name=".computers.ComputerManagerService"
|
||||||
|
android:label="Computer Management Service" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ContextMenu;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
public class AppView extends Activity {
|
||||||
|
private ListView appList;
|
||||||
|
private ArrayAdapter<AppObject> appListAdapter;
|
||||||
|
private InetAddress ipAddress;
|
||||||
|
private String uniqueId;
|
||||||
|
private boolean remote;
|
||||||
|
|
||||||
|
private final static int RESUME_ID = 1;
|
||||||
|
private final static int QUIT_ID = 2;
|
||||||
|
private final static int CANCEL_ID = 3;
|
||||||
|
|
||||||
|
public final static String ADDRESS_EXTRA = "Address";
|
||||||
|
public final static String UNIQUEID_EXTRA = "UniqueId";
|
||||||
|
public final static String NAME_EXTRA = "Name";
|
||||||
|
public final static String REMOTE_EXTRA = "Remote";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
|
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
||||||
|
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
||||||
|
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
||||||
|
if (address == null || uniqueId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
TextView label = (TextView) findViewById(R.id.appListText);
|
||||||
|
setTitle(labelText);
|
||||||
|
label.setText(labelText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ipAddress = InetAddress.getByAddress(address);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the list view
|
||||||
|
appList = (ListView)findViewById(R.id.pcListView);
|
||||||
|
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
|
||||||
|
appListAdapter.setNotifyOnChange(false);
|
||||||
|
appList.setAdapter(appListAdapter);
|
||||||
|
appList.setItemsCanFocus(true);
|
||||||
|
appList.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
AppObject app = appListAdapter.getItem(pos);
|
||||||
|
if (app == null || app.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only open the context menu if something is running, otherwise start it
|
||||||
|
if (getRunningAppId() != -1) {
|
||||||
|
openContextMenu(arg1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
doStart(app.app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(appList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
updateAppList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getRunningAppId() {
|
||||||
|
int runningAppId = -1;
|
||||||
|
for (int i = 0; i < appListAdapter.getCount(); i++) {
|
||||||
|
AppObject app = appListAdapter.getItem(i);
|
||||||
|
if (app.app == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.app.getIsRunning()) {
|
||||||
|
runningAppId = app.app.getAppId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runningAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
|
AppObject selectedApp = appListAdapter.getItem(info.position);
|
||||||
|
if (selectedApp == null || selectedApp.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int runningAppId = getRunningAppId();
|
||||||
|
if (runningAppId != -1) {
|
||||||
|
if (runningAppId == selectedApp.app.getAppId()) {
|
||||||
|
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
|
||||||
|
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
|
||||||
|
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
|
AppObject app = appListAdapter.getItem(info.position);
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case RESUME_ID:
|
||||||
|
// Resume is the same as start for us
|
||||||
|
doStart(app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case QUIT_ID:
|
||||||
|
doQuit(app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case CANCEL_ID:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateString(NvApp app) {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append(app.getAppName());
|
||||||
|
if (app.getIsRunning()) {
|
||||||
|
str.append(" - Running");
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addListPlaceholder() {
|
||||||
|
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAppList() {
|
||||||
|
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<NvApp> appList = httpConn.getAppList();
|
||||||
|
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
appListAdapter.clear();
|
||||||
|
if (appList.isEmpty()) {
|
||||||
|
addListPlaceholder();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
appListAdapter.add(new AppObject(generateString(app), app));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appListAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Success case
|
||||||
|
return;
|
||||||
|
} catch (GfeHttpResponseException ignored) {
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
} catch (XmlPullParserException ignored) {
|
||||||
|
} finally {
|
||||||
|
spinner.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doStart(NvApp app) {
|
||||||
|
Intent intent = new Intent(this, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doQuit(final NvApp app) {
|
||||||
|
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = "Successfully quit "+app.getAppName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "Failed to quit "+app.getAppName();
|
||||||
|
}
|
||||||
|
updateAppList();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = "Failed to resolve host";
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||||
|
+ "Try rebooting your machine or reinstalling GFE.";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppObject {
|
||||||
|
public String text;
|
||||||
|
public NvApp app;
|
||||||
|
|
||||||
|
public AppObject(String text, NvApp app) {
|
||||||
|
this.text = text;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,818 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
|
import com.limelight.binding.input.TouchContext;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevWatcher;
|
||||||
|
import com.limelight.binding.video.ConfigurableDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
|
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.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.graphics.Point;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
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;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
|
||||||
|
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||||
|
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||||
|
OnSystemUiVisibilityChangeListener
|
||||||
|
{
|
||||||
|
private int lastMouseX = Integer.MIN_VALUE;
|
||||||
|
private int lastMouseY = Integer.MIN_VALUE;
|
||||||
|
private int lastButtonState = 0;
|
||||||
|
|
||||||
|
// Only 2 touches are supported
|
||||||
|
private TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
|
|
||||||
|
private ControllerHandler controllerHandler;
|
||||||
|
private KeyboardTranslator keybTranslator;
|
||||||
|
|
||||||
|
private PreferenceConfiguration prefConfig;
|
||||||
|
private Point screenSize = new Point(0, 0);
|
||||||
|
|
||||||
|
private NvConnection conn;
|
||||||
|
private SpinnerDialog spinner;
|
||||||
|
private boolean displayedFailureDialog = false;
|
||||||
|
private boolean connecting = false;
|
||||||
|
private boolean connected = false;
|
||||||
|
|
||||||
|
private EvdevWatcher evdevWatcher;
|
||||||
|
private int modifierFlags = 0;
|
||||||
|
private boolean grabbedInput = true;
|
||||||
|
private boolean grabComboDown = false;
|
||||||
|
|
||||||
|
private ConfigurableDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
|
private WifiManager.WifiLock wifiLock;
|
||||||
|
|
||||||
|
private int drFlags = 0;
|
||||||
|
|
||||||
|
public static final String EXTRA_HOST = "Host";
|
||||||
|
public static final String EXTRA_APP = "App";
|
||||||
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// We don't want a title bar
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
|
// Full-screen and don't let the display go off
|
||||||
|
getWindow().addFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||||
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
|
// If we're going to use immersive mode, we want to have
|
||||||
|
// the entire screen
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Inflate the content
|
||||||
|
setContentView(R.layout.activity_game);
|
||||||
|
|
||||||
|
// Start the spinner
|
||||||
|
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
|
||||||
|
|
||||||
|
// Read the stream preferences
|
||||||
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
|
switch (prefConfig.decoder) {
|
||||||
|
case PreferenceConfiguration.FORCE_SOFTWARE_DECODER:
|
||||||
|
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
|
||||||
|
break;
|
||||||
|
case PreferenceConfiguration.AUTOSELECT_DECODER:
|
||||||
|
break;
|
||||||
|
case PreferenceConfiguration.FORCE_HARDWARE_DECODER:
|
||||||
|
drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.stretchVideo) {
|
||||||
|
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
|
display.getSize(screenSize);
|
||||||
|
|
||||||
|
// Listen for events on the game surface
|
||||||
|
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
|
||||||
|
sv.setOnGenericMotionListener(this);
|
||||||
|
sv.setOnTouchListener(this);
|
||||||
|
|
||||||
|
// Warn the user if they're on a metered connection
|
||||||
|
checkDataConnection();
|
||||||
|
|
||||||
|
// Make sure Wi-Fi is fully powered up
|
||||||
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
|
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
||||||
|
wifiLock.setReferenceCounted(false);
|
||||||
|
wifiLock.acquire();
|
||||||
|
|
||||||
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
|
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
||||||
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
|
|
||||||
|
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 (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
||||||
|
// Set the surface to the size of the video
|
||||||
|
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize touch contexts
|
||||||
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
|
touchContextMap[i] = new TouchContext(conn, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LimelightBuildProps.ROOT_BUILD) {
|
||||||
|
// Start watching for raw input
|
||||||
|
evdevWatcher = new EvdevWatcher(this);
|
||||||
|
evdevWatcher.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The connection will be started when the surface gets created
|
||||||
|
sh.addCallback(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
|
||||||
|
{
|
||||||
|
// Get the visible width of the activity
|
||||||
|
double visibleWidth = getWindow().getDecorView().getWidth();
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
||||||
|
|
||||||
|
// Calculate the new size of the SurfaceView
|
||||||
|
lp.width = (int) visibleWidth;
|
||||||
|
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
||||||
|
|
||||||
|
// Apply the size change
|
||||||
|
sv.setLayoutParams(lp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDataConnection()
|
||||||
|
{
|
||||||
|
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (mgr.isActiveNetworkMetered()) {
|
||||||
|
displayTransientMessage("Warning: Your active network connection is metered!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private Runnable hideSystemUi = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
Game.this.getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Game.this.getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||||
|
View.SYSTEM_UI_FLAG_LOW_PROFILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void hideSystemUi(int delay) {
|
||||||
|
Handler h = getWindow().getDecorView().getHandler();
|
||||||
|
if (h != null) {
|
||||||
|
h.removeCallbacks(hideSystemUi);
|
||||||
|
h.postDelayed(hideSystemUi, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
displayedFailureDialog = true;
|
||||||
|
stopConnection();
|
||||||
|
|
||||||
|
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||||
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
|
String message = null;
|
||||||
|
if (averageEndToEndLat > 0) {
|
||||||
|
message = "Average client-side frame latency: "+averageEndToEndLat+" ms";
|
||||||
|
if (averageDecoderLat > 0) {
|
||||||
|
message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (averageDecoderLat > 0) {
|
||||||
|
message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
modifier |= KeyboardPacket.MODIFIER_SHIFT;
|
||||||
|
}
|
||||||
|
if (event.isCtrlPressed()) {
|
||||||
|
modifier |= KeyboardPacket.MODIFIER_CTRL;
|
||||||
|
}
|
||||||
|
if (event.isAltPressed()) {
|
||||||
|
modifier |= KeyboardPacket.MODIFIER_ALT;
|
||||||
|
}
|
||||||
|
return modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte getModifierState() {
|
||||||
|
return (byte) modifierFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
|
InputDevice dev = event.getDevice();
|
||||||
|
if (dev == null) {
|
||||||
|
return super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass-through virtual navigation keys
|
||||||
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
|
return super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the controller handler first
|
||||||
|
boolean handled = controllerHandler.handleButtonDown(keyCode, event);
|
||||||
|
if (!handled) {
|
||||||
|
// Try the keyboard handler
|
||||||
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
|
if (translated == 0) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
|
InputDevice dev = event.getDevice();
|
||||||
|
if (dev == null) {
|
||||||
|
return super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass-through virtual navigation keys
|
||||||
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
|
return super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the controller handler first
|
||||||
|
boolean handled = controllerHandler.handleButtonUp(keyCode, event);
|
||||||
|
if (!handled) {
|
||||||
|
// Try the keyboard handler
|
||||||
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
|
if (translated == 0) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TouchContext getTouchContext(int actionIndex)
|
||||||
|
{
|
||||||
|
if (actionIndex < touchContextMap.length) {
|
||||||
|
return touchContextMap[actionIndex];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
int actionIndex = event.getActionIndex();
|
||||||
|
|
||||||
|
int eventX = (int)event.getX(actionIndex);
|
||||||
|
int eventY = (int)event.getY(actionIndex);
|
||||||
|
|
||||||
|
TouchContext context = getTouchContext(actionIndex);
|
||||||
|
if (context == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getActionMasked())
|
||||||
|
{
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
context.touchDownEvent(eventX, eventY);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
context.touchUpEvent(eventX, eventY);
|
||||||
|
if (actionIndex == 0 && event.getPointerCount() > 1) {
|
||||||
|
// The original secondary touch now becomes primary
|
||||||
|
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
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 (TouchContext aTouchContextMap : touchContextMap) {
|
||||||
|
aTouchContextMap.touchMoveEvent(eventX, eventY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This case is for mice
|
||||||
|
else if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
|
||||||
|
lastButtonState = event.getButtonState();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unknown source
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handled a known source
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown class
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
return handleMotionEvent(event) || super.onTouchEvent(event);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotionEvent(MotionEvent 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
|
||||||
|
// and the mouse coordinates change
|
||||||
|
if (lastMouseX != Integer.MIN_VALUE &&
|
||||||
|
lastMouseY != Integer.MIN_VALUE &&
|
||||||
|
!(lastMouseX == eventX && lastMouseY == eventY))
|
||||||
|
{
|
||||||
|
int deltaX = eventX - lastMouseX;
|
||||||
|
int deltaY = eventY - lastMouseY;
|
||||||
|
|
||||||
|
// Scale the deltas if the device resolution is different
|
||||||
|
// than the stream resolution
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update pointer location for delta calculation next time
|
||||||
|
lastMouseX = eventX;
|
||||||
|
lastMouseY = eventY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||||
|
return handleMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
return handleMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stageStarting(Stage stage) {
|
||||||
|
if (spinner != null) {
|
||||||
|
spinner.setMessage("Starting "+stage.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
if (spinner != null) {
|
||||||
|
spinner.dismiss();
|
||||||
|
spinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!displayedFailureDialog) {
|
||||||
|
displayedFailureDialog = true;
|
||||||
|
stopConnection();
|
||||||
|
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionTerminated(Exception e) {
|
||||||
|
if (!displayedFailureDialog) {
|
||||||
|
displayedFailureDialog = true;
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
stopConnection();
|
||||||
|
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionStarted() {
|
||||||
|
if (spinner != null) {
|
||||||
|
spinner.dismiss();
|
||||||
|
spinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connecting = false;
|
||||||
|
connected = true;
|
||||||
|
|
||||||
|
hideSystemUi(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void displayMessage(final String message) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void displayTransientMessage(final String message) {
|
||||||
|
if (!prefConfig.disableWarnings) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
|
if (!connected && !connecting) {
|
||||||
|
connecting = true;
|
||||||
|
|
||||||
|
// Resize the surface to match the aspect ratio of the video
|
||||||
|
// This must be done after the surface is created.
|
||||||
|
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
|
||||||
|
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
||||||
|
prefConfig.width, prefConfig.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||||
|
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
if (connected) {
|
||||||
|
stopConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMove(int deltaX, int deltaY) {
|
||||||
|
conn.sendMouseMove((short) deltaX, (short) deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseButtonEvent(int buttonId, boolean down) {
|
||||||
|
byte buttonIndex;
|
||||||
|
|
||||||
|
switch (buttonId)
|
||||||
|
{
|
||||||
|
case EvdevListener.BUTTON_LEFT:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_LEFT;
|
||||||
|
break;
|
||||||
|
case EvdevListener.BUTTON_MIDDLE:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_MIDDLE;
|
||||||
|
break;
|
||||||
|
case EvdevListener.BUTTON_RIGHT:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LimeLog.warning("Unhandled button: "+buttonId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (down) {
|
||||||
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseScroll(byte amount) {
|
||||||
|
conn.sendMouseScroll(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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, getModifierState());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
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;
|
||||||
|
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.preferences.AddComputerManually;
|
||||||
|
import com.limelight.preferences.StreamSettings;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
public class PcView extends Activity {
|
||||||
|
private Button settingsButton, addComputerButton;
|
||||||
|
private ListView pcList;
|
||||||
|
private ArrayAdapter<ComputerObject> pcListAdapter;
|
||||||
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private boolean freezeUpdates, runningPolling;
|
||||||
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
|
|
||||||
|
// Wait in a separate thread to avoid stalling the UI
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Wait for the binder to be ready
|
||||||
|
localBinder.waitForReady();
|
||||||
|
|
||||||
|
// Now make the binder visible
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
|
// Start updates
|
||||||
|
startComputerUpdates();
|
||||||
|
|
||||||
|
// Force a keypair to be generated early to avoid discovery delays
|
||||||
|
new AndroidCryptoProvider(PcView.this).getClientCertificate();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private void initializeViews() {
|
||||||
|
setContentView(R.layout.activity_pc_view);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
pcList.setAdapter(pcListAdapter);
|
||||||
|
pcList.setItemsCanFocus(true);
|
||||||
|
pcList.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(pos);
|
||||||
|
if (computer.details == null) {
|
||||||
|
// Placeholder item; no context menu for it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Open the context menu if a PC is offline
|
||||||
|
openContextMenu(arg1);
|
||||||
|
}
|
||||||
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
// Pair an unpaired machine by default
|
||||||
|
doPair(computer.details);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
doAppList(computer.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(pcList);
|
||||||
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
startActivity(new Intent(PcView.this, StreamSettings.class));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addComputerButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent i = new Intent(PcView.this, AddComputerManually.class);
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if (managerBinder != null) {
|
||||||
|
if (runningPolling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
freezeUpdates = false;
|
||||||
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||||
|
if (!freezeUpdates) {
|
||||||
|
PcView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
updateListView(details);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
runningPolling = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopComputerUpdates(boolean wait) {
|
||||||
|
if (managerBinder != null) {
|
||||||
|
if (!runningPolling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
freezeUpdates = true;
|
||||||
|
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
|
||||||
|
if (wait) {
|
||||||
|
managerBinder.waitForPollingStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
runningPolling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
stopComputerUpdates(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
stopComputerUpdates(false);
|
||||||
|
|
||||||
|
// Call superclass
|
||||||
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(info.position);
|
||||||
|
if (computer == null || computer.details == null) {
|
||||||
|
startComputerUpdates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inflate the context menu
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
||||||
|
}
|
||||||
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
|
||||||
|
menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doPair(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (computer.runningGameId != 0) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is currently in a game. " +
|
||||||
|
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
||||||
|
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
addr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConn = new NvHTTP(addr,
|
||||||
|
managerBinder.getUniqueId(),
|
||||||
|
PlatformBinding.getDeviceName(),
|
||||||
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
|
message = "Already paired";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final String pinStr = PairingManager.generatePinString();
|
||||||
|
|
||||||
|
// Spin the dialog off in a thread because it blocks
|
||||||
|
Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
|
||||||
|
|
||||||
|
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||||
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
|
message = "Incorrect PIN";
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.FAILED) {
|
||||||
|
message = "Pairing failed";
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
|
message = "Paired successfully";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Should be no other values
|
||||||
|
message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = "Failed to resolve host";
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||||
|
+ "Try rebooting your machine or reinstalling GFE.";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start polling again
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doWakeOnLan(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computer.macAddress == null) {
|
||||||
|
Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
WakeOnLanSender.sendWolPacket(computer);
|
||||||
|
message = "It may take a few seconds for your PC to wake up. " +
|
||||||
|
"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
|
||||||
|
} catch (IOException e) {
|
||||||
|
message = "Failed to send Wake-On-LAN packets";
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doUnpair(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
||||||
|
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
InetAddress addr = null;
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
|
addr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpConn = new NvHTTP(addr,
|
||||||
|
managerBinder.getUniqueId(),
|
||||||
|
PlatformBinding.getDeviceName(),
|
||||||
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
|
httpConn.unpair();
|
||||||
|
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
||||||
|
message = "Unpaired successfully";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "Failed to unpair";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "Device was not paired";
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = "Failed to resolve host";
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
||||||
|
+ "Try rebooting your machine or reinstalling GFE.";
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doAppList(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
||||||
|
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent i = new Intent(this, AppView.class);
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
|
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
|
||||||
|
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
|
||||||
|
i.putExtra(AppView.REMOTE_EXTRA, false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
|
||||||
|
i.putExtra(AppView.REMOTE_EXTRA, true);
|
||||||
|
}
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(info.position);
|
||||||
|
switch (item.getItemId())
|
||||||
|
{
|
||||||
|
case PAIR_ID:
|
||||||
|
doPair(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case UNPAIR_ID:
|
||||||
|
doUnpair(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case WOL_ID:
|
||||||
|
doWakeOnLan(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case DELETE_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
||||||
|
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
managerBinder.removeComputer(computer.details.name);
|
||||||
|
removeListView(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case APP_LIST_ID:
|
||||||
|
doAppList(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateString(ComputerDetails details) {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append(details.name).append(" - ");
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
str.append("Online ");
|
||||||
|
if (details.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
str.append("(Local) - ");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("(Remote) - ");
|
||||||
|
}
|
||||||
|
if (details.pairState == PairState.PAIRED) {
|
||||||
|
if (details.runningGameId == 0) {
|
||||||
|
str.append("Available");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("In Game");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("Not Paired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
str.append("Offline");
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addListPlaceholder() {
|
||||||
|
pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
|
||||||
|
"If your PC doesn't show up in about 15 seconds, " +
|
||||||
|
"make sure your computer is running GFE or add your PC manually using the button above.", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeListView(ComputerDetails details) {
|
||||||
|
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(i);
|
||||||
|
|
||||||
|
if (details.equals(computer.details)) {
|
||||||
|
pcListAdapter.remove(computer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pcListAdapter.getCount() == 0) {
|
||||||
|
// Add the placeholder if we're down to 0 computers
|
||||||
|
addListPlaceholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateListView(ComputerDetails details) {
|
||||||
|
String computerString = generateString(details);
|
||||||
|
ComputerObject existingEntry = null;
|
||||||
|
boolean placeholderPresent = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
||||||
|
ComputerObject computer = pcListAdapter.getItem(i);
|
||||||
|
|
||||||
|
// If there's a placeholder, there's nothing else
|
||||||
|
if (computer.details == null) {
|
||||||
|
placeholderPresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is the same computer
|
||||||
|
if (details.equals(computer.details)) {
|
||||||
|
existingEntry = computer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry != null) {
|
||||||
|
// Replace the information in the existing entry
|
||||||
|
existingEntry.text = computerString;
|
||||||
|
existingEntry.details = details;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If the placeholder is the only object, remove it
|
||||||
|
if (placeholderPresent) {
|
||||||
|
pcListAdapter.remove(pcListAdapter.getItem(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new entry
|
||||||
|
pcListAdapter.add(new ComputerObject(computerString, details));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the view that the data has changed
|
||||||
|
pcListAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ComputerObject {
|
||||||
|
public String text;
|
||||||
|
public ComputerDetails details;
|
||||||
|
|
||||||
|
public ComputerObject(String text, ComputerDetails details) {
|
||||||
|
this.text = text;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
/* This is a dummy class to allow for a separate icon
|
||||||
|
* and launcher for TV.
|
||||||
|
*/
|
||||||
|
public class PcViewTv extends PcView {}
|
||||||
+8
@@ -1,7 +1,11 @@
|
|||||||
package com.limelight.binding;
|
package com.limelight.binding;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
import com.limelight.binding.audio.AndroidAudioRenderer;
|
import com.limelight.binding.audio.AndroidAudioRenderer;
|
||||||
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
public class PlatformBinding {
|
public class PlatformBinding {
|
||||||
public static String getDeviceName() {
|
public static String getDeviceName() {
|
||||||
@@ -13,4 +17,8 @@ public class PlatformBinding {
|
|||||||
public static AudioRenderer getAudioRenderer() {
|
public static AudioRenderer getAudioRenderer() {
|
||||||
return new AndroidAudioRenderer();
|
return new AndroidAudioRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||||
|
return new AndroidCryptoProvider(c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+4
-2
@@ -14,7 +14,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void streamInitialized(int channelCount, int sampleRate) {
|
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
int bufferSize;
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Decoder returned unhandled channel count");
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
@@ -47,6 +48,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
AudioTrack.MODE_STREAM);
|
AudioTrack.MODE_STREAM);
|
||||||
|
|
||||||
track.play();
|
track.play();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
package com.limelight.binding.crypto;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
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.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||||
|
|
||||||
|
private File certFile;
|
||||||
|
private File keyFile;
|
||||||
|
|
||||||
|
private X509Certificate cert;
|
||||||
|
private RSAPrivateKey key;
|
||||||
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Install the Bouncy Castle provider
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidCryptoProvider(Context c) {
|
||||||
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
|
|
||||||
|
certFile = new File(dataPath + File.separator + "client.crt");
|
||||||
|
keyFile = new File(dataPath + File.separator + "client.key");
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] loadFileToBytes(File f) {
|
||||||
|
if (!f.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream fin = new FileInputStream(f);
|
||||||
|
byte[] fileData = new byte[(int) f.length()];
|
||||||
|
if (fin.read(fileData) != f.length()) {
|
||||||
|
// Failed to read
|
||||||
|
fileData = null;
|
||||||
|
}
|
||||||
|
fin.close();
|
||||||
|
return fileData;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean loadCertKeyPair() {
|
||||||
|
byte[] certBytes = loadFileToBytes(certFile);
|
||||||
|
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||||
|
|
||||||
|
// If either file was missing, we definitely can't succeed
|
||||||
|
if (certBytes == null || keyBytes == null) {
|
||||||
|
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||||
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
pemCertBytes = certBytes;
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||||
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// May happen if the cert is corrupt
|
||||||
|
LimeLog.warning("Corrupted certificate");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
// May happen if the key is corrupt
|
||||||
|
LimeLog.warning("Corrupted key");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("TrulyRandom")
|
||||||
|
private boolean generateCertKeyPair() {
|
||||||
|
byte[] snBytes = new byte[8];
|
||||||
|
new SecureRandom().nextBytes(snBytes);
|
||||||
|
|
||||||
|
KeyPair keyPair;
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
|
// Should never happen
|
||||||
|
e1.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// Expires in 20 years
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(now);
|
||||||
|
calendar.add(Calendar.YEAR, 20);
|
||||||
|
Date expirationDate = calendar.getTime();
|
||||||
|
|
||||||
|
BigInteger serial = new BigInteger(snBytes).abs();
|
||||||
|
|
||||||
|
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||||
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||||
|
X500Name name = nameBuilder.build();
|
||||||
|
|
||||||
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||||
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||||
|
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||||
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Nothing should go wrong here
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|
||||||
|
// Save the resulting pair
|
||||||
|
saveCertKeyPair();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCertKeyPair() {
|
||||||
|
try {
|
||||||
|
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||||
|
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||||
|
|
||||||
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
|
StringWriter strWriter = new StringWriter();
|
||||||
|
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||||
|
pemWriter.writeObject(cert);
|
||||||
|
pemWriter.close();
|
||||||
|
|
||||||
|
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||||
|
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||||
|
String pemStr = strWriter.getBuffer().toString();
|
||||||
|
for (int i = 0; i < pemStr.length(); i++) {
|
||||||
|
char c = pemStr.charAt(i);
|
||||||
|
if (c != '\r')
|
||||||
|
certWriter.append(c);
|
||||||
|
}
|
||||||
|
certWriter.close();
|
||||||
|
|
||||||
|
// Write the private out in PKCS8 format
|
||||||
|
keyOut.write(key.getEncoded());
|
||||||
|
|
||||||
|
certOut.close();
|
||||||
|
keyOut.close();
|
||||||
|
|
||||||
|
LimeLog.info("Saved generated key pair to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This isn't good because it means we'll have
|
||||||
|
// to re-pair next time
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getClientCertificate() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Return a loaded cert if we have one
|
||||||
|
if (cert != null) {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded cert yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Return a loaded key if we have one
|
||||||
|
if (key != null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded key yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Call our helper function to do the cert loading/generation for us
|
||||||
|
getClientCertificate();
|
||||||
|
|
||||||
|
// Return a cached value if we have it
|
||||||
|
return pemCertBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeBase64String(byte[] data) {
|
||||||
|
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
+233
-68
@@ -2,12 +2,14 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
public class ControllerHandler {
|
public class ControllerHandler {
|
||||||
private short inputMap = 0x0000;
|
private short inputMap = 0x0000;
|
||||||
@@ -17,6 +19,27 @@ public class ControllerHandler {
|
|||||||
private short rightStickY = 0x0000;
|
private short rightStickY = 0x0000;
|
||||||
private short leftStickX = 0x0000;
|
private short leftStickX = 0x0000;
|
||||||
private short leftStickY = 0x0000;
|
private short leftStickY = 0x0000;
|
||||||
|
private int emulatingButtonFlags = 0;
|
||||||
|
|
||||||
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
|
// a bumper up and compare that to our maximum delay when we receive
|
||||||
|
// a Start button press to see if we should activate one of our
|
||||||
|
// emulated button combos.
|
||||||
|
private long lastLbUpTime = 0;
|
||||||
|
private long lastRbUpTime = 0;
|
||||||
|
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||||
|
|
||||||
|
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||||
|
|
||||||
|
private static final int EMULATING_SPECIAL = 0x1;
|
||||||
|
private static final int EMULATING_SELECT = 0x2;
|
||||||
|
|
||||||
|
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
|
||||||
|
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
|
||||||
|
|
||||||
|
private Vector2d inputVector = new Vector2d();
|
||||||
|
private Vector2d normalizedInputVector = new Vector2d();
|
||||||
|
|
||||||
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
|
||||||
|
|
||||||
@@ -24,6 +47,22 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
public ControllerHandler(NvConnection conn) {
|
public ControllerHandler(NvConnection conn) {
|
||||||
this.conn = 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) {
|
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||||
@@ -32,10 +71,10 @@ public class ControllerHandler {
|
|||||||
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
|
||||||
InputDevice.MotionRange leftTriggerRange = dev.getMotionRange(MotionEvent.AXIS_LTRIGGER);
|
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||||
InputDevice.MotionRange rightTriggerRange = dev.getMotionRange(MotionEvent.AXIS_RTRIGGER);
|
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||||
InputDevice.MotionRange brakeRange = dev.getMotionRange(MotionEvent.AXIS_BRAKE);
|
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||||
InputDevice.MotionRange gasRange = dev.getMotionRange(MotionEvent.AXIS_GAS);
|
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||||
{
|
{
|
||||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||||
@@ -50,8 +89,8 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
if (rxRange != null && ryRange != null) {
|
if (rxRange != null && ryRange != null) {
|
||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||||
@@ -63,6 +102,7 @@ public class ControllerHandler {
|
|||||||
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||||
mapping.triggersIdleNegative = true;
|
mapping.triggersIdleNegative = true;
|
||||||
|
mapping.isXboxController = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// DS4 controller uses RX and RY for triggers
|
// DS4 controller uses RX and RY for triggers
|
||||||
@@ -76,8 +116,8 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||||
InputDevice.MotionRange zRange = dev.getMotionRange(MotionEvent.AXIS_Z);
|
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
|
||||||
InputDevice.MotionRange rzRange = dev.getMotionRange(MotionEvent.AXIS_RZ);
|
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
|
||||||
|
|
||||||
// Most other controllers use Z and RZ for the right stick
|
// Most other controllers use Z and RZ for the right stick
|
||||||
if (zRange != null && rzRange != null) {
|
if (zRange != null && rzRange != null) {
|
||||||
@@ -85,8 +125,8 @@ public class ControllerHandler {
|
|||||||
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
|
|
||||||
// Try RX and RY now
|
// Try RX and RY now
|
||||||
if (rxRange != null && ryRange != null) {
|
if (rxRange != null && ryRange != null) {
|
||||||
@@ -97,8 +137,8 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Some devices have "hats" for d-pads
|
// Some devices have "hats" for d-pads
|
||||||
InputDevice.MotionRange hatXRange = dev.getMotionRange(MotionEvent.AXIS_HAT_X);
|
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
|
||||||
InputDevice.MotionRange hatYRange = dev.getMotionRange(MotionEvent.AXIS_HAT_Y);
|
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
|
||||||
if (hatXRange != null && hatYRange != null) {
|
if (hatXRange != null && hatYRange != null) {
|
||||||
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||||
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||||
@@ -108,24 +148,54 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
InputDevice.MotionRange lsXRange = dev.getMotionRange(mapping.leftStickXAxis);
|
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
|
||||||
InputDevice.MotionRange lsYRange = dev.getMotionRange(mapping.leftStickYAxis);
|
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
|
||||||
if (lsXRange != null) {
|
if (lsXRange != null && lsYRange != null) {
|
||||||
mapping.leftStickXAxisDeadzone = lsXRange.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()),
|
||||||
if (lsYRange != null) {
|
Math.abs(lsYRange.getFlat()));
|
||||||
mapping.leftStickYAxisDeadzone = 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) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
InputDevice.MotionRange rsXRange = dev.getMotionRange(mapping.rightStickXAxis);
|
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
|
||||||
InputDevice.MotionRange rsYRange = dev.getMotionRange(mapping.rightStickYAxis);
|
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
|
||||||
if (rsXRange != null) {
|
if (rsXRange != null && rsYRange != null) {
|
||||||
mapping.rightStickXAxisDeadzone = rsXRange.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()),
|
||||||
if (rsYRange != null) {
|
Math.abs(rsYRange.getFlat()));
|
||||||
mapping.rightStickYAxisDeadzone = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,9 +228,9 @@ public class ControllerHandler {
|
|||||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int handleRemapping(ControllerMapping mapping, int keyCode) {
|
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||||
if (mapping.isDualShock4) {
|
if (mapping.isDualShock4) {
|
||||||
switch (keyCode) {
|
switch (event.getKeyCode()) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
|
|
||||||
@@ -199,7 +269,7 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
switch (keyCode) {
|
switch (event.getKeyCode()) {
|
||||||
// These are duplicate dpad events for hat input
|
// These are duplicate dpad events for hat input
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
@@ -209,8 +279,59 @@ public class ControllerHandler {
|
|||||||
return 0;
|
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) {
|
||||||
|
// Reinitialize our cached Vector2d object
|
||||||
|
inputVector.initialize(x, y);
|
||||||
|
|
||||||
|
if (inputVector.getMagnitude() <= deadzoneRadius) {
|
||||||
|
// Deadzone -- return the zero vector
|
||||||
|
return Vector2d.ZERO;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
else if (normalizedInputVector.getX() < -1.0f) {
|
||||||
|
normalizedInputVector.setX(-1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bound the Y value to -1.0 to 1.0
|
||||||
|
if (normalizedInputVector.getY() > 1.0f) {
|
||||||
|
normalizedInputVector.setY(1.0f);
|
||||||
|
}
|
||||||
|
else if (normalizedInputVector.getY() < -1.0f) {
|
||||||
|
normalizedInputVector.setY(-1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedInputVector;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleMotionEvent(MotionEvent event) {
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
@@ -221,30 +342,20 @@ public class ControllerHandler {
|
|||||||
|
|
||||||
// Handle left stick events outside of the deadzone
|
// Handle left stick events outside of the deadzone
|
||||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
float LS_X = event.getAxisValue(mapping.leftStickXAxis);
|
Vector2d leftStickVector = handleDeadZone(event.getAxisValue(mapping.leftStickXAxis),
|
||||||
float LS_Y = event.getAxisValue(mapping.leftStickYAxis);
|
event.getAxisValue(mapping.leftStickYAxis), mapping.leftStickDeadzoneRadius);
|
||||||
if (LS_X >= -mapping.leftStickXAxisDeadzone && LS_X <= mapping.leftStickXAxisDeadzone) {
|
|
||||||
LS_X = 0;
|
leftStickX = (short)(leftStickVector.getX() * 0x7FFE);
|
||||||
}
|
leftStickY = (short)(-leftStickVector.getY() * 0x7FFE);
|
||||||
if (LS_Y >= -mapping.leftStickYAxisDeadzone && LS_Y <= mapping.leftStickYAxisDeadzone) {
|
|
||||||
LS_Y = 0;
|
|
||||||
}
|
|
||||||
leftStickX = (short)Math.round(LS_X * 0x7FFF);
|
|
||||||
leftStickY = (short)Math.round(-LS_Y * 0x7FFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle right stick events outside of the deadzone
|
// Handle right stick events outside of the deadzone
|
||||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
float RS_X = event.getAxisValue(mapping.rightStickXAxis);
|
Vector2d rightStickVector = handleDeadZone(event.getAxisValue(mapping.rightStickXAxis),
|
||||||
float RS_Y = event.getAxisValue(mapping.rightStickYAxis);
|
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
|
||||||
if (RS_X >= -mapping.rightStickXAxisDeadzone && RS_X <= mapping.rightStickXAxisDeadzone) {
|
|
||||||
RS_X = 0;
|
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
|
||||||
}
|
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
|
||||||
if (RS_Y >= -mapping.rightStickYAxisDeadzone && RS_Y <= mapping.rightStickYAxisDeadzone) {
|
|
||||||
RS_Y = 0;
|
|
||||||
}
|
|
||||||
rightStickX = (short)Math.round(RS_X * 0x7FFF);
|
|
||||||
rightStickY = (short)Math.round(-RS_Y * 0x7FFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle controllers with analog triggers
|
// Handle controllers with analog triggers
|
||||||
@@ -257,8 +368,8 @@ public class ControllerHandler {
|
|||||||
R2 = (R2 + 1) / 2;
|
R2 = (R2 + 1) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
leftTrigger = (byte)Math.round(L2 * 0xFF);
|
leftTrigger = (byte)(L2 * 0xFF);
|
||||||
rightTrigger = (byte)Math.round(R2 * 0xFF);
|
rightTrigger = (byte)(R2 * 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hats emulate d-pad events
|
// Hats emulate d-pad events
|
||||||
@@ -293,11 +404,23 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyCode = handleRemapping(mapping, keyCode);
|
keyCode = handleRemapping(mapping, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the button hasn't been down long enough, sleep for a bit before sending the up event
|
||||||
|
// This allows "instant" button presses (like OUYA's virtual menu button) to work. This
|
||||||
|
// path should not be triggered during normal usage.
|
||||||
|
if (SystemClock.uptimeMillis() - event.getDownTime() < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
|
||||||
|
{
|
||||||
|
// Since our sleep time is so short (10 ms), it shouldn't cause a problem doing this in the
|
||||||
|
// UI thread.
|
||||||
|
try {
|
||||||
|
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KeyEvent.KEYCODE_BUTTON_MODE:
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
@@ -337,9 +460,11 @@ public class ControllerHandler {
|
|||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
inputMap &= ~ControllerPacket.LB_FLAG;
|
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||||
|
lastLbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
inputMap &= ~ControllerPacket.RB_FLAG;
|
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||||
|
lastRbUpTime = SystemClock.uptimeMillis();
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
@@ -357,11 +482,39 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If one of the two is up, the special button comes up too
|
// Check if we're emulating the select button
|
||||||
if ((inputMap & ControllerPacket.BACK_FLAG) == 0 ||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||||
(inputMap & ControllerPacket.PLAY_FLAG) == 0)
|
|
||||||
{
|
{
|
||||||
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
// If either start or LB is up, select comes up too
|
||||||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
|
(inputMap & ControllerPacket.LB_FLAG) == 0)
|
||||||
|
{
|
||||||
|
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're emulating the special button
|
||||||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
|
||||||
|
{
|
||||||
|
// If either start or select and RB is up, the special button comes up too
|
||||||
|
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
|
||||||
|
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
|
||||||
|
(inputMap & ControllerPacket.RB_FLAG) == 0))
|
||||||
|
{
|
||||||
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
sendControllerInputPacket();
|
||||||
@@ -374,7 +527,7 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyCode = handleRemapping(mapping, keyCode);
|
keyCode = handleRemapping(mapping, event);
|
||||||
if (keyCode == 0) {
|
if (keyCode == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -438,12 +591,27 @@ public class ControllerHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We detect back+start as the special button combo
|
// Start+LB acts like select for controllers with one button
|
||||||
if ((inputMap & ControllerPacket.BACK_FLAG) != 0 &&
|
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||||
|
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||||
|
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||||
|
{
|
||||||
|
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||||
|
inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We detect select+start or start+RB as the special button combo
|
||||||
|
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||||
|
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||||
|
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||||
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||||
{
|
{
|
||||||
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG);
|
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendControllerInputPacket();
|
sendControllerInputPacket();
|
||||||
@@ -451,17 +619,13 @@ public class ControllerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ControllerMapping {
|
class ControllerMapping {
|
||||||
public int leftStickXAxis = -1;
|
public int leftStickXAxis = -1;
|
||||||
public float leftStickXAxisDeadzone;
|
|
||||||
|
|
||||||
public int leftStickYAxis = -1;
|
public int leftStickYAxis = -1;
|
||||||
public float leftStickYAxisDeadzone;
|
public float leftStickDeadzoneRadius;
|
||||||
|
|
||||||
public int rightStickXAxis = -1;
|
public int rightStickXAxis = -1;
|
||||||
public float rightStickXAxisDeadzone;
|
|
||||||
|
|
||||||
public int rightStickYAxis = -1;
|
public int rightStickYAxis = -1;
|
||||||
public float rightStickYAxisDeadzone;
|
public float rightStickDeadzoneRadius;
|
||||||
|
|
||||||
public int leftTriggerAxis = -1;
|
public int leftTriggerAxis = -1;
|
||||||
public int rightTriggerAxis = -1;
|
public int rightTriggerAxis = -1;
|
||||||
@@ -473,5 +637,6 @@ public class ControllerHandler {
|
|||||||
public float hatYDeadzone;
|
public float hatYDeadzone;
|
||||||
|
|
||||||
public boolean isDualShock4;
|
public boolean isDualShock4;
|
||||||
|
public boolean isXboxController;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
public class TouchContext {
|
||||||
|
private int lastTouchX = 0;
|
||||||
|
private int lastTouchY = 0;
|
||||||
|
private int originalTouchX = 0;
|
||||||
|
private int originalTouchY = 0;
|
||||||
|
private long originalTouchTime = 0;
|
||||||
|
|
||||||
|
private NvConnection conn;
|
||||||
|
private int actionIndex;
|
||||||
|
|
||||||
|
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||||
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
|
||||||
|
public TouchContext(NvConnection conn, int actionIndex)
|
||||||
|
{
|
||||||
|
this.conn = conn;
|
||||||
|
this.actionIndex = actionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTap()
|
||||||
|
{
|
||||||
|
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
||||||
|
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
||||||
|
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||||
|
|
||||||
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte getMouseButtonIndex()
|
||||||
|
{
|
||||||
|
if (actionIndex == 1) {
|
||||||
|
return MouseButtonPacket.BUTTON_RIGHT;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return MouseButtonPacket.BUTTON_LEFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
originalTouchX = lastTouchX = eventX;
|
||||||
|
originalTouchY = lastTouchY = eventY;
|
||||||
|
originalTouchTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (isTap())
|
||||||
|
{
|
||||||
|
byte buttonIndex = getMouseButtonIndex();
|
||||||
|
|
||||||
|
// Lower the mouse button
|
||||||
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
|
||||||
|
// We need to sleep a bit here because some games
|
||||||
|
// do input detection by polling
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
|
// Raise the mouse button
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
|
{
|
||||||
|
// We only send moves for the primary touch point
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
conn.sendMouseMove((short)(eventX - lastTouchX),
|
||||||
|
(short)(eventY - lastTouchY));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchX = eventX;
|
||||||
|
lastTouchY = eventY;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
public class EvdevEvent {
|
||||||
|
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
||||||
|
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
||||||
|
|
||||||
|
/* Event types */
|
||||||
|
public static final short EV_SYN = 0x00;
|
||||||
|
public static final short EV_KEY = 0x01;
|
||||||
|
public static final short EV_REL = 0x02;
|
||||||
|
public static final short EV_MSC = 0x04;
|
||||||
|
|
||||||
|
/* Relative axes */
|
||||||
|
public static final short REL_X = 0x00;
|
||||||
|
public static final short REL_Y = 0x01;
|
||||||
|
public static final short REL_WHEEL = 0x08;
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
public static final short BTN_LEFT = 0x110;
|
||||||
|
public static final short BTN_RIGHT = 0x111;
|
||||||
|
public static final short BTN_MIDDLE = 0x112;
|
||||||
|
public static final short BTN_SIDE = 0x113;
|
||||||
|
public static final short BTN_EXTRA = 0x114;
|
||||||
|
public static final short BTN_FORWARD = 0x115;
|
||||||
|
public static final short BTN_BACK = 0x116;
|
||||||
|
public static final short BTN_TASK = 0x117;
|
||||||
|
public static final short BTN_GAMEPAD = 0x130;
|
||||||
|
|
||||||
|
/* Keys */
|
||||||
|
public static final short KEY_Q = 16;
|
||||||
|
|
||||||
|
public short type;
|
||||||
|
public short code;
|
||||||
|
public int value;
|
||||||
|
|
||||||
|
public EvdevEvent(short type, short code, int value) {
|
||||||
|
this.type = type;
|
||||||
|
this.code = code;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class EvdevHandler {
|
||||||
|
|
||||||
|
private String absolutePath;
|
||||||
|
private EvdevListener listener;
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private int fd = -1;
|
||||||
|
|
||||||
|
private Thread handlerThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// All the finally blocks here make this code look like a mess
|
||||||
|
// but it's important that we get this right to avoid causing
|
||||||
|
// system-wide input problems.
|
||||||
|
|
||||||
|
// Open the /dev/input/eventX file
|
||||||
|
fd = EvdevReader.open(absolutePath);
|
||||||
|
if (fd == -1) {
|
||||||
|
LimeLog.warning("Unable to open "+absolutePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if it's a mouse or keyboard, but not a gamepad
|
||||||
|
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||||
|
EvdevReader.isGamepad(fd)) {
|
||||||
|
// We only handle keyboards and mice
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab it for ourselves
|
||||||
|
if (!EvdevReader.grab(fd)) {
|
||||||
|
LimeLog.warning("Unable to grab "+absolutePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deltaX = 0;
|
||||||
|
int deltaY = 0;
|
||||||
|
byte deltaScroll = 0;
|
||||||
|
|
||||||
|
while (!isInterrupted() && !shutdown) {
|
||||||
|
EvdevEvent event = EvdevReader.read(fd, buffer);
|
||||||
|
if (event == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type)
|
||||||
|
{
|
||||||
|
case EvdevEvent.EV_SYN:
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
listener.mouseMove(deltaX, deltaY);
|
||||||
|
deltaX = deltaY = 0;
|
||||||
|
}
|
||||||
|
if (deltaScroll != 0) {
|
||||||
|
listener.mouseScroll(deltaScroll);
|
||||||
|
deltaScroll = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_REL:
|
||||||
|
switch (event.code)
|
||||||
|
{
|
||||||
|
case EvdevEvent.REL_X:
|
||||||
|
deltaX = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_Y:
|
||||||
|
deltaY = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_WHEEL:
|
||||||
|
deltaScroll = (byte) event.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_KEY:
|
||||||
|
switch (event.code)
|
||||||
|
{
|
||||||
|
case EvdevEvent.BTN_LEFT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_MIDDLE:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_RIGHT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.BTN_SIDE:
|
||||||
|
case EvdevEvent.BTN_EXTRA:
|
||||||
|
case EvdevEvent.BTN_FORWARD:
|
||||||
|
case EvdevEvent.BTN_BACK:
|
||||||
|
case EvdevEvent.BTN_TASK:
|
||||||
|
// Other unhandled mouse buttons
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We got some unrecognized button. This means
|
||||||
|
// someone is trying to use the other device in this
|
||||||
|
// "combination" input device. We'll try to handle
|
||||||
|
// it via keyboard, but we're not going to disconnect
|
||||||
|
// if we can't
|
||||||
|
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||||
|
if (keyCode != 0) {
|
||||||
|
listener.keyboardEvent(event.value != 0, keyCode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_MSC:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Release our grab
|
||||||
|
EvdevReader.ungrab(fd);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Close the file
|
||||||
|
EvdevReader.close(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
||||||
|
this.absolutePath = absolutePath;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
handlerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
// Close the fd. It doesn't matter if this races
|
||||||
|
// with the handler thread. We'll close this out from
|
||||||
|
// under the thread to wake it up
|
||||||
|
if (fd != -1) {
|
||||||
|
EvdevReader.close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown = true;
|
||||||
|
handlerThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
handlerThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyDeleted() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
public interface EvdevListener {
|
||||||
|
public static final int BUTTON_LEFT = 1;
|
||||||
|
public static final int BUTTON_MIDDLE = 2;
|
||||||
|
public static final int BUTTON_RIGHT = 3;
|
||||||
|
|
||||||
|
public void mouseMove(int deltaX, int deltaY);
|
||||||
|
public void mouseButtonEvent(int buttonId, boolean down);
|
||||||
|
public void mouseScroll(byte amount);
|
||||||
|
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class EvdevReader {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("evdev_reader");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires root to chmod /dev/input/eventX
|
||||||
|
public static boolean setPermissions(String[] files, int octalPermissions) {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("su");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process p = builder.start();
|
||||||
|
|
||||||
|
OutputStream stdin = p.getOutputStream();
|
||||||
|
for (String file : files) {
|
||||||
|
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
|
||||||
|
}
|
||||||
|
stdin.write("exit\n".getBytes("UTF-8"));
|
||||||
|
stdin.flush();
|
||||||
|
|
||||||
|
p.waitFor();
|
||||||
|
p.destroy();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the fd to be passed to other function or -1 on error
|
||||||
|
public static native int open(String fileName);
|
||||||
|
|
||||||
|
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||||
|
public static native boolean grab(int fd);
|
||||||
|
public static native boolean ungrab(int fd);
|
||||||
|
|
||||||
|
// Used for checking device capabilities
|
||||||
|
public static native boolean hasRelAxis(int fd, short axis);
|
||||||
|
public static native boolean hasAbsAxis(int fd, short axis);
|
||||||
|
public static native boolean hasKey(int fd, short key);
|
||||||
|
|
||||||
|
public static boolean isMouse(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||||
|
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||||
|
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAlphaKeyboard(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGamepad(int fd) {
|
||||||
|
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bytes read or -1 on error
|
||||||
|
private static native int read(int fd, byte[] buffer);
|
||||||
|
|
||||||
|
// Takes a byte buffer to use to read the output into.
|
||||||
|
// This buffer MUST be in native byte order and at least
|
||||||
|
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||||
|
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||||
|
int bytesRead = read(fd, buffer.array());
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
LimeLog.warning("Failed to read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||||
|
LimeLog.warning("Short read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.limit(bytesRead);
|
||||||
|
buffer.rewind();
|
||||||
|
|
||||||
|
// Throw away the time stamp
|
||||||
|
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||||
|
buffer.getLong();
|
||||||
|
buffer.getLong();
|
||||||
|
} else {
|
||||||
|
buffer.getInt();
|
||||||
|
buffer.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the fd from open()
|
||||||
|
public static native int close(int fd);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
public class EvdevTranslator {
|
||||||
|
|
||||||
|
public static final short EVDEV_KEY_CODES[] = {
|
||||||
|
0, //KeyEvent.VK_RESERVED
|
||||||
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
|
KeyEvent.KEYCODE_1,
|
||||||
|
KeyEvent.KEYCODE_2,
|
||||||
|
KeyEvent.KEYCODE_3,
|
||||||
|
KeyEvent.KEYCODE_4,
|
||||||
|
KeyEvent.KEYCODE_5,
|
||||||
|
KeyEvent.KEYCODE_6,
|
||||||
|
KeyEvent.KEYCODE_7,
|
||||||
|
KeyEvent.KEYCODE_8,
|
||||||
|
KeyEvent.KEYCODE_9,
|
||||||
|
KeyEvent.KEYCODE_0,
|
||||||
|
KeyEvent.KEYCODE_MINUS,
|
||||||
|
KeyEvent.KEYCODE_EQUALS,
|
||||||
|
KeyEvent.KEYCODE_DEL,
|
||||||
|
KeyEvent.KEYCODE_TAB,
|
||||||
|
KeyEvent.KEYCODE_Q,
|
||||||
|
KeyEvent.KEYCODE_W,
|
||||||
|
KeyEvent.KEYCODE_E,
|
||||||
|
KeyEvent.KEYCODE_R,
|
||||||
|
KeyEvent.KEYCODE_T,
|
||||||
|
KeyEvent.KEYCODE_Y,
|
||||||
|
KeyEvent.KEYCODE_U,
|
||||||
|
KeyEvent.KEYCODE_I,
|
||||||
|
KeyEvent.KEYCODE_O,
|
||||||
|
KeyEvent.KEYCODE_P,
|
||||||
|
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||||
|
KeyEvent.KEYCODE_A,
|
||||||
|
KeyEvent.KEYCODE_S,
|
||||||
|
KeyEvent.KEYCODE_D,
|
||||||
|
KeyEvent.KEYCODE_F,
|
||||||
|
KeyEvent.KEYCODE_G,
|
||||||
|
KeyEvent.KEYCODE_H,
|
||||||
|
KeyEvent.KEYCODE_J,
|
||||||
|
KeyEvent.KEYCODE_K,
|
||||||
|
KeyEvent.KEYCODE_L,
|
||||||
|
KeyEvent.KEYCODE_SEMICOLON,
|
||||||
|
KeyEvent.KEYCODE_APOSTROPHE,
|
||||||
|
KeyEvent.KEYCODE_GRAVE,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_BACKSLASH,
|
||||||
|
KeyEvent.KEYCODE_Z,
|
||||||
|
KeyEvent.KEYCODE_X,
|
||||||
|
KeyEvent.KEYCODE_C,
|
||||||
|
KeyEvent.KEYCODE_V,
|
||||||
|
KeyEvent.KEYCODE_B,
|
||||||
|
KeyEvent.KEYCODE_N,
|
||||||
|
KeyEvent.KEYCODE_M,
|
||||||
|
KeyEvent.KEYCODE_COMMA,
|
||||||
|
KeyEvent.KEYCODE_PERIOD,
|
||||||
|
KeyEvent.KEYCODE_SLASH,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||||
|
KeyEvent.KEYCODE_ALT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_SPACE,
|
||||||
|
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||||
|
KeyEvent.KEYCODE_F1,
|
||||||
|
KeyEvent.KEYCODE_F2,
|
||||||
|
KeyEvent.KEYCODE_F3,
|
||||||
|
KeyEvent.KEYCODE_F4,
|
||||||
|
KeyEvent.KEYCODE_F5,
|
||||||
|
KeyEvent.KEYCODE_F6,
|
||||||
|
KeyEvent.KEYCODE_F7,
|
||||||
|
KeyEvent.KEYCODE_F8,
|
||||||
|
KeyEvent.KEYCODE_F9,
|
||||||
|
KeyEvent.KEYCODE_F10,
|
||||||
|
KeyEvent.KEYCODE_NUM_LOCK,
|
||||||
|
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_7,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_8,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_9,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_4,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_5,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_6,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_1,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_2,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_3,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_0,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||||
|
0,
|
||||||
|
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||||
|
0, //KeyEvent.VK_102ND,
|
||||||
|
KeyEvent.KEYCODE_F11,
|
||||||
|
KeyEvent.KEYCODE_F12,
|
||||||
|
0, //KeyEvent.VK_RO,
|
||||||
|
0, //KeyEvent.VK_KATAKANA,
|
||||||
|
0, //KeyEvent.VK_HIRAGANA,
|
||||||
|
0, //KeyEvent.VK_HENKAN,
|
||||||
|
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||||
|
0, //KeyEvent.VK_MUHENKAN,
|
||||||
|
0, //KeyEvent.VK_KPJPCOMMA,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||||
|
KeyEvent.KEYCODE_SYSRQ,
|
||||||
|
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||||
|
0, //KeyEvent.VK_LINEFEED,
|
||||||
|
KeyEvent.KEYCODE_HOME,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_MOVE_END,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
|
KeyEvent.KEYCODE_INSERT,
|
||||||
|
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||||
|
0, //KeyEvent.VK_MACRO,
|
||||||
|
0, //KeyEvent.VK_MUTE,
|
||||||
|
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||||
|
0, //KeyEvent.VK_VOLUMEUP,
|
||||||
|
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||||
|
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||||
|
KeyEvent.KEYCODE_BREAK,
|
||||||
|
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||||
|
};
|
||||||
|
|
||||||
|
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||||
|
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||||
|
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import android.os.FileObserver;
|
||||||
|
|
||||||
|
@SuppressWarnings("ALL")
|
||||||
|
public class EvdevWatcher {
|
||||||
|
private static final String PATH = "/dev/input";
|
||||||
|
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||||
|
|
||||||
|
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private boolean init = false;
|
||||||
|
private boolean ungrabbed = false;
|
||||||
|
private EvdevListener listener;
|
||||||
|
private Thread startThread;
|
||||||
|
|
||||||
|
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||||
|
@Override
|
||||||
|
public void onEvent(int event, String fileName) {
|
||||||
|
if (fileName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (handlers) {
|
||||||
|
if (shutdown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event & FileObserver.CREATE) != 0) {
|
||||||
|
LimeLog.info("Starting evdev handler for "+fileName);
|
||||||
|
|
||||||
|
if (!init) {
|
||||||
|
// If this a real new device, update permissions again so we can read it
|
||||||
|
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
|
||||||
|
}
|
||||||
|
|
||||||
|
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||||
|
|
||||||
|
// If we're ungrabbed now, don't start the handler
|
||||||
|
if (!ungrabbed) {
|
||||||
|
handler.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.put(fileName, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event & FileObserver.DELETE) != 0) {
|
||||||
|
LimeLog.info("Halting evdev handler for "+fileName);
|
||||||
|
|
||||||
|
EvdevHandler handler = handlers.remove(fileName);
|
||||||
|
if (handler != null) {
|
||||||
|
handler.notifyDeleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevWatcher(EvdevListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File[] rundownWithPermissionsChange(int newPermissions) {
|
||||||
|
// Rundown existing files
|
||||||
|
File devInputDir = new File(PATH);
|
||||||
|
File[] files = devInputDir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return new File[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set desired permissions
|
||||||
|
String[] filePaths = new String[files.length];
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
filePaths[i] = files[i].getAbsolutePath();
|
||||||
|
}
|
||||||
|
EvdevReader.setPermissions(filePaths, newPermissions);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ungrabAll() {
|
||||||
|
synchronized (handlers) {
|
||||||
|
// Note that we're ungrabbed for now
|
||||||
|
ungrabbed = true;
|
||||||
|
|
||||||
|
// Stop all handlers
|
||||||
|
for (EvdevHandler handler : handlers.values()) {
|
||||||
|
handler.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void regrabAll() {
|
||||||
|
synchronized (handlers) {
|
||||||
|
// We're regrabbing everything now
|
||||||
|
ungrabbed = false;
|
||||||
|
|
||||||
|
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
|
||||||
|
// We need to recreate each entry since we can't reuse a stopped one
|
||||||
|
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
|
||||||
|
entry.getValue().start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
startThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// List all files and allow us access
|
||||||
|
File[] files = rundownWithPermissionsChange(0666);
|
||||||
|
|
||||||
|
init = true;
|
||||||
|
for (File f : files) {
|
||||||
|
observer.onEvent(FileObserver.CREATE, f.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done with initial onEvent calls
|
||||||
|
init = false;
|
||||||
|
|
||||||
|
// Start watching for new files
|
||||||
|
observer.startWatching();
|
||||||
|
|
||||||
|
synchronized (startThread) {
|
||||||
|
// Wait to be awoken again by shutdown()
|
||||||
|
try {
|
||||||
|
startThread.wait();
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Giveup eventX permissions
|
||||||
|
rundownWithPermissionsChange(066);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
startThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
// Let start thread cleanup on it's own sweet time
|
||||||
|
synchronized (startThread) {
|
||||||
|
startThread.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the observer
|
||||||
|
observer.stopWatching();
|
||||||
|
|
||||||
|
synchronized (handlers) {
|
||||||
|
// Stop creating new handlers
|
||||||
|
shutdown = true;
|
||||||
|
|
||||||
|
// If we've already ungrabbed, there's nothing else to do
|
||||||
|
if (ungrabbed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all handlers
|
||||||
|
for (EvdevHandler handler : handlers.values()) {
|
||||||
|
handler.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-14
@@ -5,16 +5,20 @@ import java.io.File;
|
|||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
|
import android.os.Build;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
import com.limelight.nvstream.av.DecodeUnit;
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||||
|
|
||||||
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
@@ -30,8 +34,12 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
private static final int MED_PERF = 2;
|
private static final int MED_PERF = 2;
|
||||||
private static final int HIGH_PERF = 3;
|
private static final int HIGH_PERF = 3;
|
||||||
|
|
||||||
|
private int totalFrames;
|
||||||
|
private long totalTimeMs;
|
||||||
|
|
||||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
private int findOptimalPerformanceLevel() {
|
private int findOptimalPerformanceLevel() {
|
||||||
StringBuilder cpuInfo = new StringBuilder();
|
StringBuilder cpuInfo = new StringBuilder();
|
||||||
BufferedReader br = null;
|
BufferedReader br = null;
|
||||||
@@ -49,8 +57,12 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
// We order them from greatest to least for proper detection
|
// We order them from greatest to least for proper detection
|
||||||
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
||||||
// TODO Make this better
|
// TODO Make this better (only even kind of works on ARM)
|
||||||
if (cpuInfoStr.contains("0xc0f")) {
|
if (Build.FINGERPRINT.contains("generic")) {
|
||||||
|
// Emulator
|
||||||
|
return LOW_PERF;
|
||||||
|
}
|
||||||
|
else if (cpuInfoStr.contains("0xc0f")) {
|
||||||
// Cortex-A15
|
// Cortex-A15
|
||||||
return MED_PERF;
|
return MED_PERF;
|
||||||
}
|
}
|
||||||
@@ -80,10 +92,10 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.targetFps = redrawRate;
|
this.targetFps = redrawRate;
|
||||||
|
|
||||||
int perfLevel = findOptimalPerformanceLevel();
|
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||||
int threadCount;
|
int threadCount;
|
||||||
|
|
||||||
int avcFlags = 0;
|
int avcFlags = 0;
|
||||||
@@ -138,25 +150,30 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||||
|
|
||||||
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public boolean start(final VideoDepacketizer depacketizer) {
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
|
DecodeUnit du;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
submitDecodeUnit(du);
|
||||||
|
depacketizer.freeDecodeUnit(du);
|
||||||
|
}
|
||||||
|
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
if (diff > WAIT_CEILING_MS) {
|
||||||
try {
|
LockSupport.parkNanos(1);
|
||||||
Thread.sleep(diff);
|
continue;
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||||
@@ -165,7 +182,9 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
rendererThread.setName("Video - Renderer (CPU)");
|
rendererThread.setName("Video - Renderer (CPU)");
|
||||||
|
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||||
rendererThread.start();
|
rendererThread.start();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long computePresentationTimeMs(int frameRate) {
|
private long computePresentationTimeMs(int frameRate) {
|
||||||
@@ -186,8 +205,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
AvcDecoder.destroy();
|
AvcDecoder.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||||
public boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
|
||||||
byte[] data;
|
byte[] data;
|
||||||
|
|
||||||
// Use the reserved decoder buffer if this decode unit will fit
|
// Use the reserved decoder buffer if this decode unit will fit
|
||||||
@@ -210,11 +228,36 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||||
|
if (success) {
|
||||||
|
long timeAfterDecode = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
|
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 300) {
|
||||||
|
totalTimeMs += delta;
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (totalFrames == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)(totalTimeMs / totalFrames);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
|
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
|
||||||
|
private VideoDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
decoderRenderer.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
|
if (decoderRenderer == null) {
|
||||||
|
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||||
|
}
|
||||||
|
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeWithFlags(int drFlags) {
|
||||||
|
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||||
|
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||||
|
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||||
|
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHardwareAccelerated() {
|
||||||
|
if (decoderRenderer == null) {
|
||||||
|
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||||
|
}
|
||||||
|
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean start(VideoDepacketizer depacketizer) {
|
||||||
|
return decoderRenderer.start(depacketizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
decoderRenderer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCapabilities() {
|
||||||
|
return decoderRenderer.getCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getAverageDecoderLatency();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getAverageEndToEndLatency();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
|
public class ComputerDatabaseManager {
|
||||||
|
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||||
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
|
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||||
|
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||||
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
|
|
||||||
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
|
public ComputerDatabaseManager(Context c) {
|
||||||
|
try {
|
||||||
|
// Create or open an existing DB
|
||||||
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
// Delete the DB and try again
|
||||||
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
|
}
|
||||||
|
initializeDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
computerDb.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeDb() {
|
||||||
|
// Create tables if they aren't already there
|
||||||
|
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||||
|
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||||
|
COMPUTER_TABLE_NAME,
|
||||||
|
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||||
|
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteComputer(String name) {
|
||||||
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
|
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||||
|
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||||
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ComputerDetails> getAllComputers() {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
|
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, skip the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
details.macAddress == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
computerList.add(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
return computerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerDetails getComputerByName(String name) {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
if (!c.moveToFirst()) {
|
||||||
|
// No matching computer
|
||||||
|
c.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, delete the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
details.macAddress == null) {
|
||||||
|
deleteComputer(details.name);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
public interface ComputerManagerListener {
|
||||||
|
public void notifyComputerUpdated(ComputerDetails details);
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.discovery.DiscoveryService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
public class ComputerManagerService extends Service {
|
||||||
|
private static final int POLLING_PERIOD_MS = 5000;
|
||||||
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
|
||||||
|
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
|
private ComputerDatabaseManager dbManager;
|
||||||
|
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private IdentityManager idManager;
|
||||||
|
private HashMap<ComputerDetails, Thread> pollingThreads;
|
||||||
|
private ComputerManagerListener listener = null;
|
||||||
|
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
|
|
||||||
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
synchronized (discoveryServiceConnection) {
|
||||||
|
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||||
|
|
||||||
|
// Set us as the event listener
|
||||||
|
privateBinder.setListener(createDiscoveryListener());
|
||||||
|
|
||||||
|
// Signal a possible waiter that we're all setup
|
||||||
|
discoveryBinder = privateBinder;
|
||||||
|
discoveryServiceConnection.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
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) {
|
||||||
|
// Set the listener
|
||||||
|
ComputerManagerService.this.listener = listener;
|
||||||
|
|
||||||
|
// Start mDNS autodiscovery too
|
||||||
|
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||||
|
|
||||||
|
// Start polling known machines
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
|
||||||
|
synchronized (pollingThreads) {
|
||||||
|
for (ComputerDetails computer : computerList) {
|
||||||
|
// This polling thread might already be there
|
||||||
|
if (!pollingThreads.containsKey(computer)) {
|
||||||
|
Thread t = createPollingThread(computer);
|
||||||
|
pollingThreads.put(computer, t);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void waitForReady() {
|
||||||
|
synchronized (discoveryServiceConnection) {
|
||||||
|
try {
|
||||||
|
while (discoveryBinder == null) {
|
||||||
|
// Wait for the bind notification
|
||||||
|
discoveryServiceConnection.wait(1000);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
ComputerManagerService.this.addComputer(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
ComputerManagerService.this.removeComputer(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopPolling() {
|
||||||
|
// Just call the unbind handler to cleanup
|
||||||
|
ComputerManagerService.this.onUnbind(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUniqueId() {
|
||||||
|
return idManager.getUniqueId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
// Stop mDNS autodiscovery
|
||||||
|
discoveryBinder.stopDiscovery();
|
||||||
|
|
||||||
|
// Stop polling
|
||||||
|
synchronized (pollingThreads) {
|
||||||
|
for (Thread t : pollingThreads.values()) {
|
||||||
|
t.interrupt();
|
||||||
|
}
|
||||||
|
pollingThreads.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the listener
|
||||||
|
listener = null;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||||
|
return new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
// Kick off a serverinfo poll on this machine
|
||||||
|
addComputer(computer.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
|
LimeLog.severe("mDNS discovery failed");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
|
// Block while we try to fill the details
|
||||||
|
runPoll(fakeDetails);
|
||||||
|
|
||||||
|
// If the machine is reachable, it was successful
|
||||||
|
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove it from the database
|
||||||
|
dbManager.deleteComputer(name);
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getLocalDatabaseReference() {
|
||||||
|
if (dbRefCount.get() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dbRefCount.incrementAndGet();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLocalDatabaseReference() {
|
||||||
|
if (dbRefCount.decrementAndGet() == 0) {
|
||||||
|
dbManager.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
||||||
|
ComputerDetails polledDetails;
|
||||||
|
|
||||||
|
if (localFirst) {
|
||||||
|
polledDetails = tryPollIp(details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||||
|
// Failed, so let's try the fallback
|
||||||
|
if (!localFirst) {
|
||||||
|
polledDetails = tryPollIp(details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fallback poll worked
|
||||||
|
if (polledDetails != null) {
|
||||||
|
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
|
ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (polledDetails != null) {
|
||||||
|
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||||
|
ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine was unreachable both tries
|
||||||
|
if (polledDetails == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, it's reachable
|
||||||
|
details.update(polledDetails);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean doPollMachine(ComputerDetails details) {
|
||||||
|
return pollComputer(details, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
// Bind to the discovery service
|
||||||
|
bindService(new Intent(this, DiscoveryService.class),
|
||||||
|
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
pollingThreads = new HashMap<ComputerDetails, Thread>();
|
||||||
|
|
||||||
|
// Lookup or generate this device's UID
|
||||||
|
idManager = new IdentityManager(this);
|
||||||
|
|
||||||
|
// Initialize the DB
|
||||||
|
dbManager = new ComputerDatabaseManager(this);
|
||||||
|
dbRefCount.set(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (discoveryBinder != null) {
|
||||||
|
// Unbind from the discovery service
|
||||||
|
unbindService(discoveryServiceConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
|
||||||
|
|
||||||
|
// Remove the initial DB reference
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class IdentityManager {
|
||||||
|
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
||||||
|
private static final int UID_SIZE_IN_BYTES = 8;
|
||||||
|
|
||||||
|
private String uniqueId;
|
||||||
|
|
||||||
|
public IdentityManager(Context c) {
|
||||||
|
uniqueId = loadUniqueId(c);
|
||||||
|
if (uniqueId == null) {
|
||||||
|
uniqueId = generateNewUniqueId(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("UID is now: "+uniqueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUniqueId() {
|
||||||
|
return uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadUniqueId(Context c) {
|
||||||
|
// 2 Hex digits per byte
|
||||||
|
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||||
|
InputStreamReader reader = null;
|
||||||
|
LimeLog.info("Reading UID from disk");
|
||||||
|
try {
|
||||||
|
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||||
|
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||||
|
{
|
||||||
|
LimeLog.severe("UID file data is truncated");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new String(uid);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
LimeLog.info("No UID file found");
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
LimeLog.severe("Error while reading UID file");
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
reader.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateNewUniqueId(Context c) {
|
||||||
|
// Generate a new UID hex string
|
||||||
|
LimeLog.info("Generating new UID");
|
||||||
|
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||||
|
|
||||||
|
OutputStreamWriter writer = null;
|
||||||
|
try {
|
||||||
|
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||||
|
writer.write(uidStr);
|
||||||
|
LimeLog.info("UID written to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LimeLog.severe("Error while writing UID file");
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (writer != null) {
|
||||||
|
try {
|
||||||
|
writer.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can return a UID even if I/O fails
|
||||||
|
return uidStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.limelight.discovery;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
|
||||||
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.net.wifi.WifiManager.MulticastLock;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
public class DiscoveryService extends Service {
|
||||||
|
|
||||||
|
private MdnsDiscoveryAgent discoveryAgent;
|
||||||
|
private MdnsDiscoveryListener boundListener;
|
||||||
|
private MulticastLock multicastLock;
|
||||||
|
|
||||||
|
public class DiscoveryBinder extends Binder {
|
||||||
|
public void setListener(MdnsDiscoveryListener listener) {
|
||||||
|
boundListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startDiscovery(int queryIntervalMs) {
|
||||||
|
multicastLock.acquire();
|
||||||
|
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopDiscovery() {
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
|
multicastLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MdnsComputer> getComputerSet() {
|
||||||
|
return discoveryAgent.getComputerSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
|
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||||
|
multicastLock.setReferenceCounted(false);
|
||||||
|
|
||||||
|
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerAdded(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerRemoved(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyDiscoveryFailure(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
// Stop any discovery session
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
|
multicastLock.release();
|
||||||
|
|
||||||
|
// Unbind the listener
|
||||||
|
boundListener = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -8,7 +8,6 @@ public class AvcDecoder {
|
|||||||
System.loadLibrary("swscale-2");
|
System.loadLibrary("swscale-2");
|
||||||
System.loadLibrary("avcodec-55");
|
System.loadLibrary("avcodec-55");
|
||||||
System.loadLibrary("avformat-55");
|
System.loadLibrary("avformat-55");
|
||||||
System.loadLibrary("avfilter-3");
|
|
||||||
|
|
||||||
System.loadLibrary("nv_avc_dec");
|
System.loadLibrary("nv_avc_dec");
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
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.R;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
public class AddComputerManually extends Activity {
|
||||||
|
private Button addPcButton;
|
||||||
|
private TextView hostText;
|
||||||
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||||
|
private Thread addThread;
|
||||||
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||||
|
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
|
startAddThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
joinAddThread();
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void doAddPc(String host) {
|
||||||
|
String msg;
|
||||||
|
boolean finish = false;
|
||||||
|
try {
|
||||||
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
|
|
||||||
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
|
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = "Successfully added computer";
|
||||||
|
finish = true;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean toastFinish = finish;
|
||||||
|
final String toastMsg = msg;
|
||||||
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
if (toastFinish && !isFinishing()) {
|
||||||
|
// Close the activity
|
||||||
|
AddComputerManually.this.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startAddThread() {
|
||||||
|
addThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
String computer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
computer = computersToAdd.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAddPc(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addThread.setName("UI - AddComputerManually");
|
||||||
|
addThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinAddThread() {
|
||||||
|
if (addThread != null) {
|
||||||
|
addThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
addThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
|
addThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
joinAddThread();
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
|
this.addPcButton = (Button) findViewById(R.id.addPc);
|
||||||
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
|
|
||||||
|
// Bind to the ComputerManager service
|
||||||
|
bindService(new Intent(AddComputerManually.this,
|
||||||
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
addPcButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (hostText.getText().length() == 0) {
|
||||||
|
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
|
||||||
|
computersToAdd.add(hostText.getText().toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,51 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-12
@@ -11,9 +11,9 @@ public class Dialog implements Runnable {
|
|||||||
private Activity activity;
|
private Activity activity;
|
||||||
private boolean endAfterDismiss;
|
private boolean endAfterDismiss;
|
||||||
|
|
||||||
AlertDialog alert;
|
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)
|
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
@@ -25,10 +25,15 @@ public class Dialog implements Runnable {
|
|||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
{
|
{
|
||||||
for (Dialog d : rundownDialogs)
|
synchronized (rundownDialogs) {
|
||||||
d.alert.dismiss();
|
for (Dialog d : rundownDialogs) {
|
||||||
|
if (d.alert.isShowing()) {
|
||||||
rundownDialogs.clear();
|
d.alert.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rundownDialogs.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
@@ -51,16 +56,21 @@ public class Dialog implements Runnable {
|
|||||||
|
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
alert.dismiss();
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(this);
|
rundownDialogs.remove(Dialog.this);
|
||||||
|
alert.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
if (endAfterDismiss)
|
if (endAfterDismiss) {
|
||||||
activity.finish();
|
activity.finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rundownDialogs.add(this);
|
synchronized (rundownDialogs) {
|
||||||
alert.show();
|
rundownDialogs.add(this);
|
||||||
|
alert.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+33
-12
@@ -1,6 +1,7 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
@@ -13,7 +14,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
private ProgressDialog progress;
|
private ProgressDialog progress;
|
||||||
private boolean finish;
|
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)
|
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
@@ -31,12 +32,20 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
return spinner;
|
return spinner;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs(Activity activity)
|
||||||
{
|
{
|
||||||
for (SpinnerDialog d : rundownDialogs)
|
synchronized (rundownDialogs) {
|
||||||
d.progress.dismiss();
|
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
||||||
|
while (i.hasNext()) {
|
||||||
rundownDialogs.clear();
|
SpinnerDialog dialog = i.next();
|
||||||
|
if (dialog.activity == activity) {
|
||||||
|
i.remove();
|
||||||
|
if (dialog.progress.isShowing()) {
|
||||||
|
dialog.progress.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dismiss()
|
public void dismiss()
|
||||||
@@ -57,13 +66,14 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
|
// If we're dying, don't bother doing anything
|
||||||
|
if (activity.isFinishing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (progress == null)
|
if (progress == null)
|
||||||
{
|
{
|
||||||
// If we're dying, don't bother creating a dialog
|
|
||||||
if (activity.isFinishing())
|
|
||||||
return;
|
|
||||||
|
|
||||||
progress = new ProgressDialog(activity);
|
progress = new ProgressDialog(activity);
|
||||||
|
|
||||||
progress.setTitle(title);
|
progress.setTitle(title);
|
||||||
@@ -82,16 +92,27 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
progress.setCancelable(false);
|
progress.setCancelable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.show();
|
synchronized (rundownDialogs) {
|
||||||
|
rundownDialogs.add(this);
|
||||||
|
progress.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
progress.dismiss();
|
synchronized (rundownDialogs) {
|
||||||
|
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
||||||
|
progress.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(DialogInterface dialog) {
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
rundownDialogs.remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
// This will only be called if finish was true, so we don't need to check again
|
// This will only be called if finish was true, so we don't need to check again
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
# NOTE: our armeabi-v7a libraries require NEON support
|
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
|
||||||
APP_ABI := armeabi-v7a x86
|
|
||||||
|
|
||||||
# We want an optimized build
|
# We want an optimized build
|
||||||
APP_OPTIM := release
|
APP_OPTIM := release
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Android.mk for Limelight's Evdev Reader
|
||||||
|
MY_LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
|
include $(call all-subdir-makefiles)
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#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) {
|
||||||
|
const char *path;
|
||||||
|
|
||||||
|
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
||||||
|
|
||||||
|
return open(path, O_RDWR);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// has*() and friends are based on Android's EventHub.cpp
|
||||||
|
|
||||||
|
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||||
|
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
||||||
|
|
||||||
|
return test_bit(axis, relBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||||
|
unsigned char absBitmask[(ABS_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
|
||||||
|
|
||||||
|
return test_bit(axis, absBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
|
||||||
|
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||||
|
|
||||||
|
return test_bit(key, keyBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
||||||
|
jint ret;
|
||||||
|
jbyte *data;
|
||||||
|
int pollres;
|
||||||
|
struct pollfd pollinfo;
|
||||||
|
|
||||||
|
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
||||||
|
if (data == NULL) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Failed to get byte array");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Unwait every 250 ms to return to caller if the fd is closed
|
||||||
|
pollinfo.fd = fd;
|
||||||
|
pollinfo.events = POLLIN;
|
||||||
|
pollinfo.revents = 0;
|
||||||
|
pollres = poll(&pollinfo, 1, 250);
|
||||||
|
}
|
||||||
|
while (pollres == 0);
|
||||||
|
|
||||||
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
|
// We'll have data available now
|
||||||
|
ret = read(fd, data, sizeof(struct input_event));
|
||||||
|
if (ret < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"read() failed: %d", errno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return close(fd);
|
||||||
|
}
|
||||||
@@ -12,6 +12,6 @@ LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/$(TARGET_ARCH_ABI)/include
|
|||||||
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
|
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
|
||||||
|
|
||||||
# Link to ffmpeg libraries
|
# Link to ffmpeg libraries
|
||||||
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libavfilter libwsresample
|
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libwsresample
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
@@ -24,12 +24,6 @@ LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavutil-52.so
|
|||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE:= libavfilter
|
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavfilter-3.so
|
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
LOCAL_MODULE:= libwsresample
|
LOCAL_MODULE:= libwsresample
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
|
||||||
+120
-108
@@ -37,10 +37,6 @@
|
|||||||
#include "libavutil/dict.h"
|
#include "libavutil/dict.h"
|
||||||
#include "libavutil/frame.h"
|
#include "libavutil/frame.h"
|
||||||
#include "libavutil/log.h"
|
#include "libavutil/log.h"
|
||||||
#if FF_API_FAST_MALLOC
|
|
||||||
// to provide fast_*alloc
|
|
||||||
#include "libavutil/mem.h"
|
|
||||||
#endif
|
|
||||||
#include "libavutil/pixfmt.h"
|
#include "libavutil/pixfmt.h"
|
||||||
#include "libavutil/rational.h"
|
#include "libavutil/rational.h"
|
||||||
|
|
||||||
@@ -109,9 +105,7 @@ enum AVCodecID {
|
|||||||
/* video codecs */
|
/* video codecs */
|
||||||
AV_CODEC_ID_MPEG1VIDEO,
|
AV_CODEC_ID_MPEG1VIDEO,
|
||||||
AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
||||||
#if FF_API_XVMC
|
|
||||||
AV_CODEC_ID_MPEG2VIDEO_XVMC,
|
AV_CODEC_ID_MPEG2VIDEO_XVMC,
|
||||||
#endif /* FF_API_XVMC */
|
|
||||||
AV_CODEC_ID_H261,
|
AV_CODEC_ID_H261,
|
||||||
AV_CODEC_ID_H263,
|
AV_CODEC_ID_H263,
|
||||||
AV_CODEC_ID_RV10,
|
AV_CODEC_ID_RV10,
|
||||||
@@ -282,8 +276,6 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_ESCAPE130_DEPRECATED,
|
AV_CODEC_ID_ESCAPE130_DEPRECATED,
|
||||||
AV_CODEC_ID_G2M_DEPRECATED,
|
AV_CODEC_ID_G2M_DEPRECATED,
|
||||||
AV_CODEC_ID_WEBP_DEPRECATED,
|
AV_CODEC_ID_WEBP_DEPRECATED,
|
||||||
AV_CODEC_ID_HNM4_VIDEO,
|
|
||||||
AV_CODEC_ID_HEVC_DEPRECATED,
|
|
||||||
|
|
||||||
AV_CODEC_ID_BRENDER_PIX= MKBETAG('B','P','I','X'),
|
AV_CODEC_ID_BRENDER_PIX= MKBETAG('B','P','I','X'),
|
||||||
AV_CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
|
AV_CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
|
||||||
@@ -634,26 +626,16 @@ enum AVColorPrimaries{
|
|||||||
AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
|
AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
|
||||||
AVCOL_PRI_SMPTE240M = 7, ///< functionally identical to above
|
AVCOL_PRI_SMPTE240M = 7, ///< functionally identical to above
|
||||||
AVCOL_PRI_FILM = 8,
|
AVCOL_PRI_FILM = 8,
|
||||||
AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
|
|
||||||
AVCOL_PRI_NB , ///< Not part of ABI
|
AVCOL_PRI_NB , ///< Not part of ABI
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AVColorTransferCharacteristic{
|
enum AVColorTransferCharacteristic{
|
||||||
AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
|
AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
|
||||||
AVCOL_TRC_UNSPECIFIED = 2,
|
AVCOL_TRC_UNSPECIFIED = 2,
|
||||||
AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
|
AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
|
||||||
AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
|
AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
|
||||||
AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC
|
AVCOL_TRC_SMPTE240M = 7,
|
||||||
AVCOL_TRC_SMPTE240M = 7,
|
AVCOL_TRC_NB , ///< Not part of ABI
|
||||||
AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
|
|
||||||
AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
|
|
||||||
AVCOL_TRC_LOG_SQRT = 10, ///< "Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range)"
|
|
||||||
AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
|
|
||||||
AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
|
|
||||||
AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
|
|
||||||
AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10 bit system
|
|
||||||
AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12 bit system
|
|
||||||
AVCOL_TRC_NB , ///< Not part of ABI
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -695,12 +677,7 @@ typedef struct RcOverride{
|
|||||||
float quality_factor;
|
float quality_factor;
|
||||||
} RcOverride;
|
} RcOverride;
|
||||||
|
|
||||||
#if FF_API_MAX_BFRAMES
|
|
||||||
/**
|
|
||||||
* @deprecated there is no libavcodec-wide limit on the number of B-frames
|
|
||||||
*/
|
|
||||||
#define FF_MAX_B_FRAMES 16
|
#define FF_MAX_B_FRAMES 16
|
||||||
#endif
|
|
||||||
|
|
||||||
/* encoding support
|
/* encoding support
|
||||||
These flags can be passed in AVCodecContext.flags before initialization.
|
These flags can be passed in AVCodecContext.flags before initialization.
|
||||||
@@ -714,7 +691,6 @@ typedef struct RcOverride{
|
|||||||
#define CODEC_FLAG_UNALIGNED 0x0001
|
#define CODEC_FLAG_UNALIGNED 0x0001
|
||||||
#define CODEC_FLAG_QSCALE 0x0002 ///< Use fixed qscale.
|
#define CODEC_FLAG_QSCALE 0x0002 ///< Use fixed qscale.
|
||||||
#define CODEC_FLAG_4MV 0x0004 ///< 4 MV per MB allowed / advanced prediction for H.263.
|
#define CODEC_FLAG_4MV 0x0004 ///< 4 MV per MB allowed / advanced prediction for H.263.
|
||||||
#define CODEC_FLAG_OUTPUT_CORRUPT 0x0008 ///< Output even those frames that might be corrupted
|
|
||||||
#define CODEC_FLAG_QPEL 0x0010 ///< Use qpel MC.
|
#define CODEC_FLAG_QPEL 0x0010 ///< Use qpel MC.
|
||||||
#define CODEC_FLAG_GMC 0x0020 ///< Use GMC.
|
#define CODEC_FLAG_GMC 0x0020 ///< Use GMC.
|
||||||
#define CODEC_FLAG_MV0 0x0040 ///< Always try a MB with MV=<0,0>.
|
#define CODEC_FLAG_MV0 0x0040 ///< Always try a MB with MV=<0,0>.
|
||||||
@@ -765,10 +741,8 @@ typedef struct RcOverride{
|
|||||||
*/
|
*/
|
||||||
#define CODEC_CAP_DR1 0x0002
|
#define CODEC_CAP_DR1 0x0002
|
||||||
#define CODEC_CAP_TRUNCATED 0x0008
|
#define CODEC_CAP_TRUNCATED 0x0008
|
||||||
#if FF_API_XVMC
|
|
||||||
/* Codec can export data for HW decoding (XvMC). */
|
/* Codec can export data for HW decoding (XvMC). */
|
||||||
#define CODEC_CAP_HWACCEL 0x0010
|
#define CODEC_CAP_HWACCEL 0x0010
|
||||||
#endif /* FF_API_XVMC */
|
|
||||||
/**
|
/**
|
||||||
* Encoder or decoder requires flushing with NULL input at the end in order to
|
* Encoder or decoder requires flushing with NULL input at the end in order to
|
||||||
* give the complete and correct output.
|
* give the complete and correct output.
|
||||||
@@ -825,12 +799,12 @@ typedef struct RcOverride{
|
|||||||
* Codec should fill in channel configuration and samplerate instead of container
|
* Codec should fill in channel configuration and samplerate instead of container
|
||||||
*/
|
*/
|
||||||
#define CODEC_CAP_CHANNEL_CONF 0x0400
|
#define CODEC_CAP_CHANNEL_CONF 0x0400
|
||||||
#if FF_API_NEG_LINESIZES
|
|
||||||
/**
|
/**
|
||||||
* @deprecated no codecs use this capability
|
* Codec is able to deal with negative linesizes
|
||||||
*/
|
*/
|
||||||
#define CODEC_CAP_NEG_LINESIZES 0x0800
|
#define CODEC_CAP_NEG_LINESIZES 0x0800
|
||||||
#endif
|
|
||||||
/**
|
/**
|
||||||
* Codec supports frame-level multithreading.
|
* Codec supports frame-level multithreading.
|
||||||
*/
|
*/
|
||||||
@@ -860,7 +834,6 @@ typedef struct RcOverride{
|
|||||||
*/
|
*/
|
||||||
#define CODEC_CAP_LOSSLESS 0x80000000
|
#define CODEC_CAP_LOSSLESS 0x80000000
|
||||||
|
|
||||||
#if FF_API_MB_TYPE
|
|
||||||
//The following defines may change, don't expect compatibility if you use them.
|
//The following defines may change, don't expect compatibility if you use them.
|
||||||
#define MB_TYPE_INTRA4x4 0x0001
|
#define MB_TYPE_INTRA4x4 0x0001
|
||||||
#define MB_TYPE_INTRA16x16 0x0002 //FIXME H.264-specific
|
#define MB_TYPE_INTRA16x16 0x0002 //FIXME H.264-specific
|
||||||
@@ -884,7 +857,6 @@ typedef struct RcOverride{
|
|||||||
#define MB_TYPE_QUANT 0x00010000
|
#define MB_TYPE_QUANT 0x00010000
|
||||||
#define MB_TYPE_CBP 0x00020000
|
#define MB_TYPE_CBP 0x00020000
|
||||||
//Note bits 24-31 are reserved for codec specific use (h264 ref0, mpeg1 0mv, ...)
|
//Note bits 24-31 are reserved for codec specific use (h264 ref0, mpeg1 0mv, ...)
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pan Scan area.
|
* Pan Scan area.
|
||||||
@@ -915,12 +887,10 @@ typedef struct AVPanScan{
|
|||||||
int16_t position[3][2];
|
int16_t position[3][2];
|
||||||
}AVPanScan;
|
}AVPanScan;
|
||||||
|
|
||||||
#if FF_API_QSCALE_TYPE
|
|
||||||
#define FF_QSCALE_TYPE_MPEG1 0
|
#define FF_QSCALE_TYPE_MPEG1 0
|
||||||
#define FF_QSCALE_TYPE_MPEG2 1
|
#define FF_QSCALE_TYPE_MPEG2 1
|
||||||
#define FF_QSCALE_TYPE_H264 2
|
#define FF_QSCALE_TYPE_H264 2
|
||||||
#define FF_QSCALE_TYPE_VP56 3
|
#define FF_QSCALE_TYPE_VP56 3
|
||||||
#endif
|
|
||||||
|
|
||||||
#if FF_API_GET_BUFFER
|
#if FF_API_GET_BUFFER
|
||||||
#define FF_BUFFER_TYPE_INTERNAL 1
|
#define FF_BUFFER_TYPE_INTERNAL 1
|
||||||
@@ -1353,9 +1323,7 @@ typedef struct AVCodecContext {
|
|||||||
*/
|
*/
|
||||||
int coded_width, coded_height;
|
int coded_width, coded_height;
|
||||||
|
|
||||||
#if FF_API_ASPECT_EXTENDED
|
|
||||||
#define FF_ASPECT_EXTENDED 15
|
#define FF_ASPECT_EXTENDED 15
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the number of pictures in a group of pictures, or 0 for intra_only
|
* the number of pictures in a group of pictures, or 0 for intra_only
|
||||||
@@ -1682,15 +1650,12 @@ typedef struct AVCodecContext {
|
|||||||
#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics)
|
#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics)
|
||||||
#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
|
#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
|
||||||
|
|
||||||
#if FF_API_XVMC
|
|
||||||
/**
|
/**
|
||||||
* XVideo Motion Acceleration
|
* XVideo Motion Acceleration
|
||||||
* - encoding: forbidden
|
* - encoding: forbidden
|
||||||
* - decoding: set by decoder
|
* - decoding: set by decoder
|
||||||
* @deprecated XvMC support is slated for removal.
|
|
||||||
*/
|
*/
|
||||||
attribute_deprecated int xvmc_acceleration;
|
int xvmc_acceleration;
|
||||||
#endif /* FF_API_XVMC */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* macroblock decision mode
|
* macroblock decision mode
|
||||||
@@ -2425,16 +2390,12 @@ typedef struct AVCodecContext {
|
|||||||
*/
|
*/
|
||||||
int workaround_bugs;
|
int workaround_bugs;
|
||||||
#define FF_BUG_AUTODETECT 1 ///< autodetection
|
#define FF_BUG_AUTODETECT 1 ///< autodetection
|
||||||
#if FF_API_OLD_MSMPEG4
|
|
||||||
#define FF_BUG_OLD_MSMPEG4 2
|
#define FF_BUG_OLD_MSMPEG4 2
|
||||||
#endif
|
|
||||||
#define FF_BUG_XVID_ILACE 4
|
#define FF_BUG_XVID_ILACE 4
|
||||||
#define FF_BUG_UMP4 8
|
#define FF_BUG_UMP4 8
|
||||||
#define FF_BUG_NO_PADDING 16
|
#define FF_BUG_NO_PADDING 16
|
||||||
#define FF_BUG_AMV 32
|
#define FF_BUG_AMV 32
|
||||||
#if FF_API_AC_VLC
|
|
||||||
#define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default.
|
#define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default.
|
||||||
#endif
|
|
||||||
#define FF_BUG_QPEL_CHROMA 64
|
#define FF_BUG_QPEL_CHROMA 64
|
||||||
#define FF_BUG_STD_QPEL 128
|
#define FF_BUG_STD_QPEL 128
|
||||||
#define FF_BUG_QPEL_CHROMA2 256
|
#define FF_BUG_QPEL_CHROMA2 256
|
||||||
@@ -2484,12 +2445,7 @@ typedef struct AVCodecContext {
|
|||||||
#define FF_DEBUG_BITSTREAM 4
|
#define FF_DEBUG_BITSTREAM 4
|
||||||
#define FF_DEBUG_MB_TYPE 8
|
#define FF_DEBUG_MB_TYPE 8
|
||||||
#define FF_DEBUG_QP 16
|
#define FF_DEBUG_QP 16
|
||||||
#if FF_API_DEBUG_MV
|
|
||||||
/**
|
|
||||||
* @deprecated this option does nothing
|
|
||||||
*/
|
|
||||||
#define FF_DEBUG_MV 32
|
#define FF_DEBUG_MV 32
|
||||||
#endif
|
|
||||||
#define FF_DEBUG_DCT_COEFF 0x00000040
|
#define FF_DEBUG_DCT_COEFF 0x00000040
|
||||||
#define FF_DEBUG_SKIP 0x00000080
|
#define FF_DEBUG_SKIP 0x00000080
|
||||||
#define FF_DEBUG_STARTCODE 0x00000100
|
#define FF_DEBUG_STARTCODE 0x00000100
|
||||||
@@ -2497,17 +2453,13 @@ typedef struct AVCodecContext {
|
|||||||
#define FF_DEBUG_ER 0x00000400
|
#define FF_DEBUG_ER 0x00000400
|
||||||
#define FF_DEBUG_MMCO 0x00000800
|
#define FF_DEBUG_MMCO 0x00000800
|
||||||
#define FF_DEBUG_BUGS 0x00001000
|
#define FF_DEBUG_BUGS 0x00001000
|
||||||
#if FF_API_DEBUG_MV
|
#define FF_DEBUG_VIS_QP 0x00002000
|
||||||
#define FF_DEBUG_VIS_QP 0x00002000 ///< only access through AVOptions from outside libavcodec
|
#define FF_DEBUG_VIS_MB_TYPE 0x00004000
|
||||||
#define FF_DEBUG_VIS_MB_TYPE 0x00004000 ///< only access through AVOptions from outside libavcodec
|
|
||||||
#endif
|
|
||||||
#define FF_DEBUG_BUFFERS 0x00008000
|
#define FF_DEBUG_BUFFERS 0x00008000
|
||||||
#define FF_DEBUG_THREADS 0x00010000
|
#define FF_DEBUG_THREADS 0x00010000
|
||||||
|
|
||||||
#if FF_API_DEBUG_MV
|
|
||||||
/**
|
/**
|
||||||
* debug
|
* debug
|
||||||
* Code outside libavcodec should access this field using AVOptions
|
|
||||||
* - encoding: Set by user.
|
* - encoding: Set by user.
|
||||||
* - decoding: Set by user.
|
* - decoding: Set by user.
|
||||||
*/
|
*/
|
||||||
@@ -2515,7 +2467,6 @@ typedef struct AVCodecContext {
|
|||||||
#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
|
#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
|
||||||
#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
|
#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
|
||||||
#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
|
#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error recognition; may misdetect some more or less valid parts as errors.
|
* Error recognition; may misdetect some more or less valid parts as errors.
|
||||||
@@ -2523,14 +2474,7 @@ typedef struct AVCodecContext {
|
|||||||
* - decoding: Set by user.
|
* - decoding: Set by user.
|
||||||
*/
|
*/
|
||||||
int err_recognition;
|
int err_recognition;
|
||||||
|
#define AV_EF_CRCCHECK (1<<0) ///< verify embedded CRCs
|
||||||
/**
|
|
||||||
* Verify checksums embedded in the bitstream (could be of either encoded or
|
|
||||||
* decoded data, depending on the codec) and print an error message on mismatch.
|
|
||||||
* If AV_EF_EXPLODE is also set, a mismatching checksum will result in the
|
|
||||||
* decoder returning an error.
|
|
||||||
*/
|
|
||||||
#define AV_EF_CRCCHECK (1<<0)
|
|
||||||
#define AV_EF_BITSTREAM (1<<1) ///< detect bitstream specification deviations
|
#define AV_EF_BITSTREAM (1<<1) ///< detect bitstream specification deviations
|
||||||
#define AV_EF_BUFFER (1<<2) ///< detect improper bitstream length
|
#define AV_EF_BUFFER (1<<2) ///< detect improper bitstream length
|
||||||
#define AV_EF_EXPLODE (1<<3) ///< abort decoding on minor error detection
|
#define AV_EF_EXPLODE (1<<3) ///< abort decoding on minor error detection
|
||||||
@@ -2609,9 +2553,7 @@ typedef struct AVCodecContext {
|
|||||||
#define FF_IDCT_SIMPLEVIS 18
|
#define FF_IDCT_SIMPLEVIS 18
|
||||||
#define FF_IDCT_FAAN 20
|
#define FF_IDCT_FAAN 20
|
||||||
#define FF_IDCT_SIMPLENEON 22
|
#define FF_IDCT_SIMPLENEON 22
|
||||||
#if FF_API_ARCH_ALPHA
|
|
||||||
#define FF_IDCT_SIMPLEALPHA 23
|
#define FF_IDCT_SIMPLEALPHA 23
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bits per sample/pixel from the demuxer (needed for huffyuv).
|
* bits per sample/pixel from the demuxer (needed for huffyuv).
|
||||||
@@ -2641,7 +2583,7 @@ typedef struct AVCodecContext {
|
|||||||
/**
|
/**
|
||||||
* the picture in the bitstream
|
* the picture in the bitstream
|
||||||
* - encoding: Set by libavcodec.
|
* - encoding: Set by libavcodec.
|
||||||
* - decoding: unused
|
* - decoding: Set by libavcodec.
|
||||||
*/
|
*/
|
||||||
AVFrame *coded_frame;
|
AVFrame *coded_frame;
|
||||||
|
|
||||||
@@ -2713,13 +2655,13 @@ typedef struct AVCodecContext {
|
|||||||
*/
|
*/
|
||||||
int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
|
int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
|
||||||
|
|
||||||
#if FF_API_THREAD_OPAQUE
|
|
||||||
/**
|
/**
|
||||||
* @deprecated this field should not be used from outside of lavc
|
* thread opaque
|
||||||
|
* Can be used by execute() to store some per AVCodecContext stuff.
|
||||||
|
* - encoding: set by execute()
|
||||||
|
* - decoding: set by execute()
|
||||||
*/
|
*/
|
||||||
attribute_deprecated
|
|
||||||
void *thread_opaque;
|
void *thread_opaque;
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* noise vs. sse weight for the nsse comparsion function
|
* noise vs. sse weight for the nsse comparsion function
|
||||||
@@ -2846,22 +2788,21 @@ typedef struct AVCodecContext {
|
|||||||
uint8_t *subtitle_header;
|
uint8_t *subtitle_header;
|
||||||
int subtitle_header_size;
|
int subtitle_header_size;
|
||||||
|
|
||||||
#if FF_API_ERROR_RATE
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use the 'error_rate' private AVOption of the mpegvideo
|
* Simulates errors in the bitstream to test error concealment.
|
||||||
* encoders
|
* - encoding: Set by user.
|
||||||
|
* - decoding: unused
|
||||||
*/
|
*/
|
||||||
attribute_deprecated
|
|
||||||
int error_rate;
|
int error_rate;
|
||||||
#endif
|
|
||||||
|
|
||||||
#if FF_API_CODEC_PKT
|
|
||||||
/**
|
/**
|
||||||
* @deprecated this field is not supposed to be accessed from outside lavc
|
* Current packet as passed into the decoder, to avoid having
|
||||||
|
* to pass the packet into every function. Currently only valid
|
||||||
|
* inside lavc and get/release_buffer callbacks.
|
||||||
|
* - decoding: set by avcodec_decode_*, read by get_buffer() for setting pkt_pts
|
||||||
|
* - encoding: unused
|
||||||
*/
|
*/
|
||||||
attribute_deprecated
|
|
||||||
AVPacket *pkt;
|
AVPacket *pkt;
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VBV delay coded in the last frame (in periods of a 27 MHz clock).
|
* VBV delay coded in the last frame (in periods of a 27 MHz clock).
|
||||||
@@ -2949,19 +2890,6 @@ typedef struct AVCodecContext {
|
|||||||
* - encoding: set by libavcodec
|
* - encoding: set by libavcodec
|
||||||
*/
|
*/
|
||||||
int seek_preroll;
|
int seek_preroll;
|
||||||
|
|
||||||
#if !FF_API_DEBUG_MV
|
|
||||||
/**
|
|
||||||
* debug motion vectors
|
|
||||||
* Code outside libavcodec should access this field using AVOptions
|
|
||||||
* - encoding: Set by user.
|
|
||||||
* - decoding: Set by user.
|
|
||||||
*/
|
|
||||||
int debug_mv;
|
|
||||||
#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
|
|
||||||
#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
|
|
||||||
#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
|
|
||||||
#endif
|
|
||||||
} AVCodecContext;
|
} AVCodecContext;
|
||||||
|
|
||||||
AVRational av_codec_get_pkt_timebase (const AVCodecContext *avctx);
|
AVRational av_codec_get_pkt_timebase (const AVCodecContext *avctx);
|
||||||
@@ -3298,6 +3226,40 @@ void avcodec_register(AVCodec *codec);
|
|||||||
*/
|
*/
|
||||||
void avcodec_register_all(void);
|
void avcodec_register_all(void);
|
||||||
|
|
||||||
|
|
||||||
|
#if FF_API_ALLOC_CONTEXT
|
||||||
|
/**
|
||||||
|
* Allocate an AVCodecContext and set its fields to default values. The
|
||||||
|
* resulting struct can be deallocated by simply calling av_free().
|
||||||
|
*
|
||||||
|
* @return An AVCodecContext filled with default values or NULL on failure.
|
||||||
|
* @see avcodec_get_context_defaults
|
||||||
|
*
|
||||||
|
* @deprecated use avcodec_alloc_context3()
|
||||||
|
*/
|
||||||
|
attribute_deprecated
|
||||||
|
AVCodecContext *avcodec_alloc_context(void);
|
||||||
|
|
||||||
|
/** THIS FUNCTION IS NOT YET PART OF THE PUBLIC API!
|
||||||
|
* we WILL change its arguments and name a few times! */
|
||||||
|
attribute_deprecated
|
||||||
|
AVCodecContext *avcodec_alloc_context2(enum AVMediaType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the fields of the given AVCodecContext to default values.
|
||||||
|
*
|
||||||
|
* @param s The AVCodecContext of which the fields should be set to default values.
|
||||||
|
* @deprecated use avcodec_get_context_defaults3
|
||||||
|
*/
|
||||||
|
attribute_deprecated
|
||||||
|
void avcodec_get_context_defaults(AVCodecContext *s);
|
||||||
|
|
||||||
|
/** THIS FUNCTION IS NOT YET PART OF THE PUBLIC API!
|
||||||
|
* we WILL change its arguments and name a few times! */
|
||||||
|
attribute_deprecated
|
||||||
|
void avcodec_get_context_defaults2(AVCodecContext *s, enum AVMediaType);
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocate an AVCodecContext and set its fields to default values. The
|
* Allocate an AVCodecContext and set its fields to default values. The
|
||||||
* resulting struct can be deallocated by calling avcodec_close() on it followed
|
* resulting struct can be deallocated by calling avcodec_close() on it followed
|
||||||
@@ -3363,13 +3325,14 @@ const AVClass *avcodec_get_subtitle_rect_class(void);
|
|||||||
*/
|
*/
|
||||||
int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
|
int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
|
||||||
|
|
||||||
#if FF_API_AVFRAME_LAVC
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use av_frame_alloc()
|
* Allocate an AVFrame and set its fields to default values. The resulting
|
||||||
|
* struct must be freed using avcodec_free_frame().
|
||||||
|
*
|
||||||
|
* @return An AVFrame filled with default values or NULL on failure.
|
||||||
|
* @see avcodec_get_frame_defaults
|
||||||
*/
|
*/
|
||||||
attribute_deprecated
|
|
||||||
AVFrame *avcodec_alloc_frame(void);
|
AVFrame *avcodec_alloc_frame(void);
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the fields of the given AVFrame to default values.
|
* Set the fields of the given AVFrame to default values.
|
||||||
@@ -3390,6 +3353,40 @@ void avcodec_get_frame_defaults(AVFrame *frame);
|
|||||||
*/
|
*/
|
||||||
void avcodec_free_frame(AVFrame **frame);
|
void avcodec_free_frame(AVFrame **frame);
|
||||||
|
|
||||||
|
#if FF_API_AVCODEC_OPEN
|
||||||
|
/**
|
||||||
|
* Initialize the AVCodecContext to use the given AVCodec. Prior to using this
|
||||||
|
* function the context has to be allocated.
|
||||||
|
*
|
||||||
|
* The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
|
||||||
|
* avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
|
||||||
|
* retrieving a codec.
|
||||||
|
*
|
||||||
|
* @warning This function is not thread safe!
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* avcodec_register_all();
|
||||||
|
* codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
|
* if (!codec)
|
||||||
|
* exit(1);
|
||||||
|
*
|
||||||
|
* context = avcodec_alloc_context3(codec);
|
||||||
|
*
|
||||||
|
* if (avcodec_open(context, codec) < 0)
|
||||||
|
* exit(1);
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @param avctx The context which will be set up to use the given codec.
|
||||||
|
* @param codec The codec to use within the context.
|
||||||
|
* @return zero on success, a negative value on error
|
||||||
|
* @see avcodec_alloc_context3, avcodec_find_decoder, avcodec_find_encoder, avcodec_close
|
||||||
|
*
|
||||||
|
* @deprecated use avcodec_open2
|
||||||
|
*/
|
||||||
|
attribute_deprecated
|
||||||
|
int avcodec_open(AVCodecContext *avctx, AVCodec *codec);
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the AVCodecContext to use the given AVCodec. Prior to using this
|
* Initialize the AVCodecContext to use the given AVCodec. Prior to using this
|
||||||
* function the context has to be allocated with avcodec_alloc_context3().
|
* function the context has to be allocated with avcodec_alloc_context3().
|
||||||
@@ -4669,13 +4666,7 @@ enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext *s, const en
|
|||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#if FF_API_SET_DIMENSIONS
|
|
||||||
/**
|
|
||||||
* @deprecated this function is not supposed to be used from outside of lavc
|
|
||||||
*/
|
|
||||||
attribute_deprecated
|
|
||||||
void avcodec_set_dimensions(AVCodecContext *s, int width, int height);
|
void avcodec_set_dimensions(AVCodecContext *s, int width, int height);
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put a string representing the codec tag codec_tag in buf.
|
* Put a string representing the codec tag codec_tag in buf.
|
||||||
@@ -4871,6 +4862,27 @@ AVBitStreamFilter *av_bitstream_filter_next(AVBitStreamFilter *f);
|
|||||||
|
|
||||||
/* memory */
|
/* memory */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reallocate the given block if it is not large enough, otherwise do nothing.
|
||||||
|
*
|
||||||
|
* @see av_realloc
|
||||||
|
*/
|
||||||
|
void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate a buffer, reusing the given one if large enough.
|
||||||
|
*
|
||||||
|
* Contrary to av_fast_realloc the current buffer contents might not be
|
||||||
|
* preserved and on error the old buffer is freed, thus no special
|
||||||
|
* handling to avoid memleaks is necessary.
|
||||||
|
*
|
||||||
|
* @param ptr pointer to pointer to already allocated buffer, overwritten with pointer to new buffer
|
||||||
|
* @param size size of the buffer *ptr points to
|
||||||
|
* @param min_size minimum size of *ptr buffer after returning, *ptr will be NULL and
|
||||||
|
* *size 0 if an error occurred.
|
||||||
|
*/
|
||||||
|
void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same behaviour av_fast_malloc but the buffer has additional
|
* Same behaviour av_fast_malloc but the buffer has additional
|
||||||
* FF_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0.
|
* FF_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0.
|
||||||
-2
@@ -34,9 +34,7 @@
|
|||||||
/* video codecs */
|
/* video codecs */
|
||||||
CODEC_ID_MPEG1VIDEO,
|
CODEC_ID_MPEG1VIDEO,
|
||||||
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
||||||
#if FF_API_XVMC
|
|
||||||
CODEC_ID_MPEG2VIDEO_XVMC,
|
CODEC_ID_MPEG2VIDEO_XVMC,
|
||||||
#endif
|
|
||||||
CODEC_ID_H261,
|
CODEC_ID_H261,
|
||||||
CODEC_ID_H263,
|
CODEC_ID_H263,
|
||||||
CODEC_ID_RV10,
|
CODEC_ID_RV10,
|
||||||
+6
-26
@@ -54,8 +54,12 @@
|
|||||||
#include "libavutil/avconfig.h"
|
#include "libavutil/avconfig.h"
|
||||||
#include "libavutil/attributes.h"
|
#include "libavutil/attributes.h"
|
||||||
|
|
||||||
#include "avcodec.h"
|
#ifndef FF_API_CAP_VDPAU
|
||||||
#include "version.h"
|
#define FF_API_CAP_VDPAU 1
|
||||||
|
#endif
|
||||||
|
#ifndef FF_API_BUFS_VDPAU
|
||||||
|
#define FF_API_BUFS_VDPAU 1
|
||||||
|
#endif
|
||||||
|
|
||||||
#if FF_API_BUFS_VDPAU
|
#if FF_API_BUFS_VDPAU
|
||||||
union AVVDPAUPictureInfo {
|
union AVVDPAUPictureInfo {
|
||||||
@@ -82,10 +86,6 @@ typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *,
|
|||||||
* during initialization or through each AVCodecContext.get_buffer()
|
* during initialization or through each AVCodecContext.get_buffer()
|
||||||
* function call. In any case, they must be valid prior to calling
|
* function call. In any case, they must be valid prior to calling
|
||||||
* decoding functions.
|
* decoding functions.
|
||||||
*
|
|
||||||
* The size of this structure is not a part of the public ABI and must not
|
|
||||||
* be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
|
|
||||||
* AVVDPAUContext.
|
|
||||||
*/
|
*/
|
||||||
typedef struct AVVDPAUContext {
|
typedef struct AVVDPAUContext {
|
||||||
/**
|
/**
|
||||||
@@ -149,26 +149,6 @@ AVVDPAUContext *av_alloc_vdpaucontext(void);
|
|||||||
AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
|
AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
|
||||||
void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
|
void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
|
||||||
|
|
||||||
/**
|
|
||||||
* Allocate an AVVDPAUContext.
|
|
||||||
*
|
|
||||||
* @return Newly-allocated AVVDPAUContext or NULL on failure.
|
|
||||||
*/
|
|
||||||
AVVDPAUContext *av_vdpau_alloc_context(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a decoder profile that should be used for initializing a VDPAU decoder.
|
|
||||||
* Should be called from the AVCodecContext.get_format() callback.
|
|
||||||
*
|
|
||||||
* @param avctx the codec context being used for decoding the stream
|
|
||||||
* @param profile a pointer into which the result will be written on success.
|
|
||||||
* The contents of profile are undefined if this function returns
|
|
||||||
* an error.
|
|
||||||
*
|
|
||||||
* @return 0 on success (non-negative), a negative AVERROR on failure.
|
|
||||||
*/
|
|
||||||
int av_vdpau_get_profile(AVCodecContext *avctx, VdpDecoderProfile *profile);
|
|
||||||
|
|
||||||
#if FF_API_CAP_VDPAU
|
#if FF_API_CAP_VDPAU
|
||||||
/** @brief The videoSurface is used for rendering. */
|
/** @brief The videoSurface is used for rendering. */
|
||||||
#define FF_VDPAU_STATE_USED_FOR_RENDER 1
|
#define FF_VDPAU_STATE_USED_FOR_RENDER 1
|
||||||
+12
-50
@@ -29,7 +29,7 @@
|
|||||||
#include "libavutil/avutil.h"
|
#include "libavutil/avutil.h"
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_MAJOR 55
|
#define LIBAVCODEC_VERSION_MAJOR 55
|
||||||
#define LIBAVCODEC_VERSION_MINOR 43
|
#define LIBAVCODEC_VERSION_MINOR 39
|
||||||
#define LIBAVCODEC_VERSION_MICRO 101
|
#define LIBAVCODEC_VERSION_MICRO 101
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
||||||
@@ -51,9 +51,19 @@
|
|||||||
#ifndef FF_API_REQUEST_CHANNELS
|
#ifndef FF_API_REQUEST_CHANNELS
|
||||||
#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 56)
|
#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef FF_API_ALLOC_CONTEXT
|
||||||
|
#define FF_API_ALLOC_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 55)
|
||||||
|
#endif
|
||||||
|
#ifndef FF_API_AVCODEC_OPEN
|
||||||
|
#define FF_API_AVCODEC_OPEN (LIBAVCODEC_VERSION_MAJOR < 55)
|
||||||
|
#endif
|
||||||
#ifndef FF_API_OLD_DECODE_AUDIO
|
#ifndef FF_API_OLD_DECODE_AUDIO
|
||||||
#define FF_API_OLD_DECODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
|
#define FF_API_OLD_DECODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef FF_API_OLD_TIMECODE
|
||||||
|
#define FF_API_OLD_TIMECODE (LIBAVCODEC_VERSION_MAJOR < 55)
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef FF_API_OLD_ENCODE_AUDIO
|
#ifndef FF_API_OLD_ENCODE_AUDIO
|
||||||
#define FF_API_OLD_ENCODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
|
#define FF_API_OLD_ENCODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
#endif
|
#endif
|
||||||
@@ -63,11 +73,8 @@
|
|||||||
#ifndef FF_API_CODEC_ID
|
#ifndef FF_API_CODEC_ID
|
||||||
#define FF_API_CODEC_ID (LIBAVCODEC_VERSION_MAJOR < 56)
|
#define FF_API_CODEC_ID (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
#endif
|
#endif
|
||||||
#ifndef FF_API_AUDIO_CONVERT
|
|
||||||
#define FF_API_AUDIO_CONVERT (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_AVCODEC_RESAMPLE
|
#ifndef FF_API_AVCODEC_RESAMPLE
|
||||||
#define FF_API_AVCODEC_RESAMPLE FF_API_AUDIO_CONVERT
|
#define FF_API_AVCODEC_RESAMPLE (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
#endif
|
#endif
|
||||||
#ifndef FF_API_DEINTERLACE
|
#ifndef FF_API_DEINTERLACE
|
||||||
#define FF_API_DEINTERLACE (LIBAVCODEC_VERSION_MAJOR < 56)
|
#define FF_API_DEINTERLACE (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
@@ -93,50 +100,5 @@
|
|||||||
#ifndef FF_API_VOXWARE
|
#ifndef FF_API_VOXWARE
|
||||||
#define FF_API_VOXWARE (LIBAVCODEC_VERSION_MAJOR < 56)
|
#define FF_API_VOXWARE (LIBAVCODEC_VERSION_MAJOR < 56)
|
||||||
#endif
|
#endif
|
||||||
#ifndef FF_API_SET_DIMENSIONS
|
|
||||||
#define FF_API_SET_DIMENSIONS (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_DEBUG_MV
|
|
||||||
#define FF_API_DEBUG_MV (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_AC_VLC
|
|
||||||
#define FF_API_AC_VLC (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_OLD_MSMPEG4
|
|
||||||
#define FF_API_OLD_MSMPEG4 (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_ASPECT_EXTENDED
|
|
||||||
#define FF_API_ASPECT_EXTENDED (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_THREAD_OPAQUE
|
|
||||||
#define FF_API_THREAD_OPAQUE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_CODEC_PKT
|
|
||||||
#define FF_API_CODEC_PKT (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_ARCH_ALPHA
|
|
||||||
#define FF_API_ARCH_ALPHA (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_XVMC
|
|
||||||
#define FF_API_XVMC (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_ERROR_RATE
|
|
||||||
#define FF_API_ERROR_RATE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_QSCALE_TYPE
|
|
||||||
#define FF_API_QSCALE_TYPE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_MB_TYPE
|
|
||||||
#define FF_API_MB_TYPE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_MAX_BFRAMES
|
|
||||||
#define FF_API_MAX_BFRAMES (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_FAST_MALLOC
|
|
||||||
#define FF_API_FAST_MALLOC (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_NEG_LINESIZES
|
|
||||||
#define FF_API_NEG_LINESIZES (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* AVCODEC_VERSION_H */
|
#endif /* AVCODEC_VERSION_H */
|
||||||
+1
-7
@@ -29,12 +29,8 @@
|
|||||||
|
|
||||||
#include <X11/extensions/XvMC.h>
|
#include <X11/extensions/XvMC.h>
|
||||||
|
|
||||||
#include "libavutil/attributes.h"
|
|
||||||
#include "version.h"
|
|
||||||
#include "avcodec.h"
|
#include "avcodec.h"
|
||||||
|
|
||||||
#if FF_API_XVMC
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @defgroup lavc_codec_hwaccel_xvmc XvMC
|
* @defgroup lavc_codec_hwaccel_xvmc XvMC
|
||||||
* @ingroup lavc_codec_hwaccel
|
* @ingroup lavc_codec_hwaccel
|
||||||
@@ -45,7 +41,7 @@
|
|||||||
#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct
|
#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct
|
||||||
the number is 1337 speak for the letters IDCT MCo (motion compensation) */
|
the number is 1337 speak for the letters IDCT MCo (motion compensation) */
|
||||||
|
|
||||||
attribute_deprecated struct xvmc_pix_fmt {
|
struct xvmc_pix_fmt {
|
||||||
/** The field contains the special constant value AV_XVMC_ID.
|
/** The field contains the special constant value AV_XVMC_ID.
|
||||||
It is used as a test that the application correctly uses the API,
|
It is used as a test that the application correctly uses the API,
|
||||||
and that there is no corruption caused by pixel routines.
|
and that there is no corruption caused by pixel routines.
|
||||||
@@ -169,6 +165,4 @@ attribute_deprecated struct xvmc_pix_fmt {
|
|||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#endif /* FF_API_XVMC */
|
|
||||||
|
|
||||||
#endif /* AVCODEC_XVMC_H */
|
#endif /* AVCODEC_XVMC_H */
|
||||||
+1
-5
@@ -775,11 +775,6 @@ typedef struct AVStream {
|
|||||||
|
|
||||||
int pts_wrap_bits; /**< number of bits in pts (used for wrapping control) */
|
int pts_wrap_bits; /**< number of bits in pts (used for wrapping control) */
|
||||||
|
|
||||||
#if FF_API_REFERENCE_DTS
|
|
||||||
/* a hack to keep ABI compatibility for ffmpeg and other applications, which accesses parser even
|
|
||||||
* though it should not */
|
|
||||||
int64_t do_not_use;
|
|
||||||
#endif
|
|
||||||
// Timestamp generation support:
|
// Timestamp generation support:
|
||||||
/**
|
/**
|
||||||
* Timestamp corresponding to the last dts sync point.
|
* Timestamp corresponding to the last dts sync point.
|
||||||
@@ -788,6 +783,7 @@ typedef struct AVStream {
|
|||||||
* a DTS is received from the underlying container. Otherwise set to
|
* a DTS is received from the underlying container. Otherwise set to
|
||||||
* AV_NOPTS_VALUE by default.
|
* AV_NOPTS_VALUE by default.
|
||||||
*/
|
*/
|
||||||
|
int64_t reference_dts;
|
||||||
int64_t first_dts;
|
int64_t first_dts;
|
||||||
int64_t cur_dts;
|
int64_t cur_dts;
|
||||||
int64_t last_IP_pts;
|
int64_t last_IP_pts;
|
||||||
+2
-5
@@ -30,8 +30,8 @@
|
|||||||
#include "libavutil/avutil.h"
|
#include "libavutil/avutil.h"
|
||||||
|
|
||||||
#define LIBAVFORMAT_VERSION_MAJOR 55
|
#define LIBAVFORMAT_VERSION_MAJOR 55
|
||||||
#define LIBAVFORMAT_VERSION_MINOR 21
|
#define LIBAVFORMAT_VERSION_MINOR 19
|
||||||
#define LIBAVFORMAT_VERSION_MICRO 100
|
#define LIBAVFORMAT_VERSION_MICRO 104
|
||||||
|
|
||||||
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
|
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
|
||||||
LIBAVFORMAT_VERSION_MINOR, \
|
LIBAVFORMAT_VERSION_MINOR, \
|
||||||
@@ -48,9 +48,6 @@
|
|||||||
* dropped at a future version bump. The defines themselves are not part of
|
* dropped at a future version bump. The defines themselves are not part of
|
||||||
* the public API and may change, break or disappear at any time.
|
* the public API and may change, break or disappear at any time.
|
||||||
*/
|
*/
|
||||||
#ifndef FF_API_REFERENCE_DTS
|
|
||||||
#define FF_API_REFERENCE_DTS (LIBAVFORMAT_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef FF_API_ALLOC_OUTPUT_CONTEXT
|
#ifndef FF_API_ALLOC_OUTPUT_CONTEXT
|
||||||
#define FF_API_ALLOC_OUTPUT_CONTEXT (LIBAVFORMAT_VERSION_MAJOR < 56)
|
#define FF_API_ALLOC_OUTPUT_CONTEXT (LIBAVFORMAT_VERSION_MAJOR < 56)
|
||||||
-3
@@ -25,9 +25,6 @@
|
|||||||
#include "attributes.h"
|
#include "attributes.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @file
|
|
||||||
* Public header for libavutil Adler32 hasher
|
|
||||||
*
|
|
||||||
* @defgroup lavu_adler32 Adler32
|
* @defgroup lavu_adler32 Adler32
|
||||||
* @ingroup lavu_crypto
|
* @ingroup lavu_crypto
|
||||||
* @{
|
* @{
|
||||||
+1
-1
@@ -76,7 +76,7 @@
|
|||||||
# define av_cold
|
# define av_cold
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if AV_GCC_VERSION_AT_LEAST(4,1) && !defined(__llvm__)
|
#if AV_GCC_VERSION_AT_LEAST(4,1)
|
||||||
# define av_flatten __attribute__((flatten))
|
# define av_flatten __attribute__((flatten))
|
||||||
#else
|
#else
|
||||||
# define av_flatten
|
# define av_flatten
|
||||||
-7
@@ -312,13 +312,6 @@ unsigned av_int_list_length_for_size(unsigned elsize,
|
|||||||
#define av_int_list_length(list, term) \
|
#define av_int_list_length(list, term) \
|
||||||
av_int_list_length_for_size(sizeof(*(list)), list, term)
|
av_int_list_length_for_size(sizeof(*(list)), list, term)
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a file using a UTF-8 filename.
|
|
||||||
* The API of this function matches POSIX fopen(), errors are returned through
|
|
||||||
* errno.
|
|
||||||
*/
|
|
||||||
FILE *av_fopen_utf8(const char *path, const char *mode);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
* @}
|
* @}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user