mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 07:24:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be153b84cb | ||
|
|
06c53e2251 | ||
|
|
695519bdf5 | ||
|
|
bf7d033ab2 | ||
|
|
df67795c4a | ||
|
|
72c1696f43 | ||
|
|
8eca3683c9 | ||
|
|
80c17b4913 | ||
|
|
e5050f10bb | ||
|
|
e912e4de57 | ||
|
|
8dee1f0d80 | ||
|
|
53594ada66 | ||
|
|
848ed1ad72 | ||
|
|
307e807c8f | ||
|
|
6a27780d56 | ||
|
|
57f98dbb4a | ||
|
|
5af7d83ec1 | ||
|
|
4a6f77f43a | ||
|
|
c96f9fb635 | ||
|
|
e3a477a243 | ||
|
|
9fcd641143 | ||
|
|
6d1cbc5a64 | ||
|
|
ec71060d98 | ||
|
|
03f706fb85 | ||
|
|
7ad87bd3ee | ||
|
|
4e088f6183 | ||
|
|
1b16ea6f53 | ||
|
|
f262503bc8 | ||
|
|
b2ba216cd1 | ||
|
|
94ba7f8e45 | ||
|
|
a267cf59c7 | ||
|
|
79e8bef289 | ||
|
|
99e3b5f33b | ||
|
|
afbe64f3ff | ||
|
|
43b1a73ae0 | ||
|
|
d08eeb8a2d | ||
|
|
7c39e5c974 | ||
|
|
cd49334199 | ||
|
|
dd59f0bc6d | ||
|
|
cf2d83a1ea | ||
|
|
d5b6130936 | ||
|
|
4ae29b0075 | ||
|
|
34e35cd493 | ||
|
|
a17af070c5 | ||
|
|
fbe0a26800 | ||
|
|
25ad99df94 | ||
|
|
6338e7b8eb | ||
|
|
1b9846d519 | ||
|
|
a4ece13a1d | ||
|
|
066b8430a0 | ||
|
|
2b54a91f3d | ||
|
|
2d01633372 | ||
|
|
5dc01069fc | ||
|
|
d450008833 | ||
|
|
a37fff6eb5 | ||
|
|
6604675bf9 | ||
|
|
1965cc2347 | ||
|
|
312ca27906 | ||
|
|
0bceadbd9a | ||
|
|
dfc3daabcd | ||
|
|
b9ba9adc1f | ||
|
|
f112d45e1a | ||
|
|
88f139873c | ||
|
|
d317c5bf03 | ||
|
|
9d72314b9c | ||
|
|
2cc7243573 | ||
|
|
269d9a6bc6 | ||
|
|
244130fc1b | ||
|
|
a67791b8aa | ||
|
|
21e46a5c3b | ||
|
|
2df2f850d5 | ||
|
|
406d26ec1c | ||
|
|
68c1aaf433 | ||
|
|
9ef577dbdd | ||
|
|
982ecbc015 | ||
|
|
7e44b5abd5 | ||
|
|
6dbb1a0c1f | ||
|
|
94b1c04fa6 | ||
|
|
9758276f1c | ||
|
|
971263c52d | ||
|
|
9b58e7bb4d | ||
|
|
69ecf0251d | ||
|
|
350a4d8825 | ||
|
|
44f447df7b | ||
|
|
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 | ||
|
|
5efcd606e3 | ||
|
|
3524cdd764 | ||
|
|
368cd8808d | ||
|
|
b52a6ce93c | ||
|
|
7ab4e5d0a5 | ||
|
|
095dfd8035 | ||
|
|
f7c33ef975 | ||
|
|
57b0bce5a4 | ||
|
|
7a017d7b97 | ||
|
|
d2773be32e | ||
|
|
93a7d9f181 | ||
|
|
9703cf4ffe | ||
|
|
41ec64e87c | ||
|
|
aca92a5056 | ||
|
|
fc9c4d9aaa | ||
|
|
d8c6a544f0 | ||
|
|
7e100f2c9c | ||
|
|
643b644e17 | ||
|
|
48a9d3ac12 | ||
|
|
efdd1e2046 | ||
|
|
8a40892865 | ||
|
|
20635a3012 | ||
|
|
b908c5cec3 | ||
|
|
1cb0b723f6 | ||
|
|
3f3c573c79 | ||
|
|
e1253bbb59 | ||
|
|
b2eb953f45 | ||
|
|
9638c15c93 | ||
|
|
8ce972ea7a | ||
|
|
968557d3a8 | ||
|
|
cf80a7380c | ||
|
|
ceb957e7f3 | ||
|
|
669691d8e8 | ||
|
|
ea57e48ed5 | ||
|
|
f744c7d9c4 | ||
|
|
0d42beca93 | ||
|
|
bafb9e6230 | ||
|
|
5d30a3f4ab | ||
|
|
70a1dd56b4 | ||
|
|
9857017b22 | ||
|
|
53474c7d28 | ||
|
|
283c1060a3 | ||
|
|
eb7c54b95c | ||
|
|
735ad02fb4 | ||
|
|
c1ad6f83b9 | ||
|
|
209b649eb8 | ||
|
|
9bf7bd366f | ||
|
|
cfd788bec0 | ||
|
|
5284db761c | ||
|
|
07b5e9224c | ||
|
|
f31736ee2d | ||
|
|
47a3a959e5 | ||
|
|
c2a679e5ef | ||
|
|
71b4980553 | ||
|
|
0b360f8f1d | ||
|
|
52823df62f | ||
|
|
39472bdc7b | ||
|
|
49ae5a6d0c | ||
|
|
1c426417f3 | ||
|
|
2cd41b5141 | ||
|
|
74d9afd685 | ||
|
|
e13241eebf | ||
|
|
700ec361b0 | ||
|
|
c7f26a179d | ||
|
|
4b2552ed64 | ||
|
|
ddc70e39c9 | ||
|
|
61c0de6ca1 | ||
|
|
b0c4d47962 | ||
|
|
6fc848ef56 | ||
|
|
c537af2273 | ||
|
|
839540dc74 | ||
|
|
22ecdd9cf8 | ||
|
|
bf1c7dd675 | ||
|
|
426b40ae82 | ||
|
|
6683c25a39 |
No files matched your search
-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
|
||||||
Whitespace-only changes.
@@ -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="1"
|
|
||||||
android:versionName="1.0" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="16"
|
|
||||||
android:targetSdkVersion="19" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:largeHeap="true"
|
|
||||||
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="orientation|keyboardHidden|screenSize"
|
|
||||||
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,58 +1,51 @@
|
|||||||
#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
|
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
Windows PC to your Android device on the same network.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
Streaming can be done remotely using the [Shield Proxy](http://forum.xda-developers.com/showthread.php?t=2435481)
|
[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.
|
||||||
application.
|
|
||||||
|
|
||||||
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OSX and Linux.
|
|
||||||
|
|
||||||
##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 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
|
||||||
|
|
||||||
* Ensure your Android device and your PC are on the same network or you're
|
* Turn on GameStream in the GFE settings
|
||||||
running [Shield Proxy](http://forum.xda-developers.com/showthread.php?t=2435481).
|
* If you are connecting from outside the same network, turn on internet
|
||||||
* Turn on Shield Streaming in the GFE settings
|
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
|
||||||
|
|
||||||
This project is being actively developed at [XDA](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
1. Fork us
|
1. Fork us
|
||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
|
Check out our [website](http://limelight-stream.com) for project links and information.
|
||||||
|
|
||||||
##Authors
|
##Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
|
|||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
<?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="okhttp-2.1.0-RC1" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="androidasync-1.3.7" 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" />
|
||||||
|
<orderEntry type="library" exported="" name="support-v4-r6" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okio-1.0.1" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="ion-1.3.7" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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 "3.0"
|
||||||
|
versionCode = 45
|
||||||
|
}
|
||||||
|
|
||||||
|
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 group: 'com.google.android', name: 'support-v4', version:'r6'
|
||||||
|
compile group: 'com.koushikdutta.ion', name: 'ion', version:'1.3.7'
|
||||||
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1'
|
||||||
|
compile group: 'com.squareup.okio', name:'okio', version:'1.0.1'
|
||||||
|
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,81 @@
|
|||||||
|
<?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:theme="@style/StreamTheme"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
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,298 @@
|
|||||||
|
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.grid.AppGridAdapter;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
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.GridView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
public class AppView extends Activity {
|
||||||
|
private AppGridAdapter appGridAdapter;
|
||||||
|
private InetAddress ipAddress;
|
||||||
|
private String uniqueId;
|
||||||
|
private boolean remote;
|
||||||
|
private boolean firstLoad = true;
|
||||||
|
|
||||||
|
private final static int RESUME_ID = 1;
|
||||||
|
private final static int QUIT_ID = 2;
|
||||||
|
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);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
||||||
|
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
||||||
|
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
||||||
|
if (address == null || uniqueId == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
TextView label = (TextView) findViewById(R.id.appListText);
|
||||||
|
setTitle(labelText);
|
||||||
|
label.setText(labelText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ipAddress = InetAddress.getByAddress(address);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the list view
|
||||||
|
GridView appGrid = (GridView) findViewById(R.id.appGridView);
|
||||||
|
try {
|
||||||
|
appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appGrid.setAdapter(appGridAdapter);
|
||||||
|
appGrid.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
if (app == null || app.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only open the context menu if something is running, otherwise start it
|
||||||
|
if (getRunningAppId() != -1) {
|
||||||
|
openContextMenu(arg1);
|
||||||
|
} else {
|
||||||
|
doStart(app.app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(appGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
// Display the error message if it was the
|
||||||
|
// first load, but just kill the activity
|
||||||
|
// on subsequent errors
|
||||||
|
updateAppList(firstLoad);
|
||||||
|
firstLoad = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getRunningAppId() {
|
||||||
|
int runningAppId = -1;
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (app.app == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.app.getIsRunning()) {
|
||||||
|
runningAppId = app.app.getAppId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runningAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
|
if (selectedApp == null || selectedApp.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int runningAppId = getRunningAppId();
|
||||||
|
if (runningAppId != -1) {
|
||||||
|
if (runningAppId == selectedApp.app.getAppId()) {
|
||||||
|
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||||
|
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case RESUME_ID:
|
||||||
|
// Resume is the same as start for us
|
||||||
|
doStart(app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case QUIT_ID:
|
||||||
|
doQuit(app.app);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case CANCEL_ID:
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAppList(final boolean displayError) {
|
||||||
|
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
||||||
|
getResources().getString(R.string.applist_refresh_msg), true);
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<NvApp> appList = httpConn.getAppList();
|
||||||
|
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
appGridAdapter.clear();
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
appGridAdapter.addApp(new AppObject(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
appGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Success case
|
||||||
|
return;
|
||||||
|
} catch (GfeHttpResponseException ignored) {
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
} catch (XmlPullParserException ignored) {
|
||||||
|
} finally {
|
||||||
|
spinner.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayError) {
|
||||||
|
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
|
||||||
|
getResources().getString(R.string.applist_refresh_error_msg), true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Just finish the activity immediately
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doStart(NvApp app) {
|
||||||
|
Intent intent = new Intent(this, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doQuit(final NvApp app) {
|
||||||
|
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
||||||
|
}
|
||||||
|
updateAppList(true);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AppObject {
|
||||||
|
public NvApp app;
|
||||||
|
|
||||||
|
public AppObject(NvApp app) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return app.getAppName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,841 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
|
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, getResources().getString(R.string.conn_establishing_title),
|
||||||
|
getResources().getString(R.string.conn_establishing_msg), 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);
|
||||||
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
|
||||||
|
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)
|
||||||
|
.setMaxPacketSize(remote ? 1024 : 1392)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Initialize the connection
|
||||||
|
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
||||||
|
keybTranslator = new KeyboardTranslator(conn);
|
||||||
|
controllerHandler = new ControllerHandler(conn, prefConfig.deadzonePercentage);
|
||||||
|
|
||||||
|
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,
|
||||||
|
((double)prefConfig.width / (double)screenSize.x),
|
||||||
|
((double)prefConfig.height / (double)screenSize.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(getResources().getString(R.string.conn_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 = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||||
|
if (averageDecoderLat > 0) {
|
||||||
|
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (averageDecoderLat > 0) {
|
||||||
|
message = getResources().getString(R.string.conn_hardware_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) {
|
||||||
|
// 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(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) {
|
||||||
|
// 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(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
|
||||||
|
|
||||||
|
// First process the historical events
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
for (TouchContext aTouchContextMap : touchContextMap) {
|
||||||
|
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
|
||||||
|
{
|
||||||
|
aTouchContextMap.touchMoveEvent(
|
||||||
|
(int)event.getHistoricalX(aTouchContextMap.getActionIndex(), i),
|
||||||
|
(int)event.getHistoricalY(aTouchContextMap.getActionIndex(), i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
|
for (TouchContext aTouchContextMap : touchContextMap) {
|
||||||
|
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
|
||||||
|
{
|
||||||
|
aTouchContextMap.touchMoveEvent(
|
||||||
|
(int)event.getX(aTouchContextMap.getActionIndex()),
|
||||||
|
(int)event.getY(aTouchContextMap.getActionIndex()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First process the history
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
|
||||||
|
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(getResources().getString(R.string.conn_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, getResources().getString(R.string.conn_error_title),
|
||||||
|
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionTerminated(Exception e) {
|
||||||
|
if (!displayedFailureDialog) {
|
||||||
|
displayedFailureDialog = true;
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
stopConnection();
|
||||||
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||||
|
getResources().getString(R.string.conn_terminated_msg), 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,576 @@
|
|||||||
|
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.grid.PcGridAdapter;
|
||||||
|
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 com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.view.ContextMenu;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
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.GridView;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
public class PcView extends Activity {
|
||||||
|
private RelativeLayout noPcFoundLayout;
|
||||||
|
private PcGridAdapter pcGridAdapter;
|
||||||
|
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);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
// Set default preferences if we've never been run
|
||||||
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
|
// Setup the list view
|
||||||
|
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
||||||
|
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
||||||
|
|
||||||
|
GridView pcGrid = (GridView) findViewById(R.id.pcGridView);
|
||||||
|
pcGrid.setAdapter(pcGridAdapter);
|
||||||
|
pcGrid.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
|
// Do nothing
|
||||||
|
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Open the context menu if a PC is offline
|
||||||
|
openContextMenu(arg1);
|
||||||
|
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
// Pair an unpaired machine by default
|
||||||
|
doPair(computer.details);
|
||||||
|
} else {
|
||||||
|
doAppList(computer.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(pcGrid);
|
||||||
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
startActivity(new Intent(PcView.this, StreamSettings.class));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addComputerButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Intent i = new Intent(PcView.this, AddComputerManually.class);
|
||||||
|
startActivity(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
||||||
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Bind to the computer manager service
|
||||||
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
pcGridAdapter = new PcGridAdapter(this);
|
||||||
|
|
||||||
|
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() {
|
||||||
|
updateComputer(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 = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
|
if (computer == null || computer.details == null ||
|
||||||
|
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
|
startComputerUpdates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inflate the context menu
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
|
}
|
||||||
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
|
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doPair(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (computer.runningGameId != 0) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
// Stop updates and wait while pairing
|
||||||
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Don't display any toast, but open the app list
|
||||||
|
message = null;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final String pinStr = PairingManager.generatePinString();
|
||||||
|
|
||||||
|
// Spin the dialog off in a thread because it blocks
|
||||||
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||||
|
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||||
|
|
||||||
|
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||||
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
|
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.FAILED) {
|
||||||
|
message = getResources().getString(R.string.pair_fail);
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
|
// Just navigate to the app view without displaying a toast
|
||||||
|
message = null;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Should be no other values
|
||||||
|
message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
final boolean toastSuccess = success;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (toastMessage != null) {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastSuccess) {
|
||||||
|
// Open the app list after a successful pairing attemp
|
||||||
|
doAppList(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start polling again
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doWakeOnLan(final ComputerDetails computer) {
|
||||||
|
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computer.macAddress == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
WakeOnLanSender.sendWolPacket(computer);
|
||||||
|
message = getResources().getString(R.string.wol_waking_msg);
|
||||||
|
} catch (IOException e) {
|
||||||
|
message = getResources().getString(R.string.wol_fail);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.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 = getResources().getString(R.string.unpair_success);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = getResources().getString(R.string.unpair_fail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = getResources().getString(R.string.unpair_error);
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doAppList(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), 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 = (ComputerObject) pcGridAdapter.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, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
managerBinder.removeComputer(computer.details.name);
|
||||||
|
removeComputer(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case APP_LIST_ID:
|
||||||
|
doAppList(computer.details);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeComputer(ComputerDetails details) {
|
||||||
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
if (details.equals(computer.details)) {
|
||||||
|
pcGridAdapter.removeComputer(computer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateComputer(ComputerDetails details) {
|
||||||
|
ComputerObject existingEntry = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// Check if this is the same computer
|
||||||
|
if (details.equals(computer.details)) {
|
||||||
|
existingEntry = computer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry != null) {
|
||||||
|
// Replace the information in the existing entry
|
||||||
|
existingEntry.details = details;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Add a new entry
|
||||||
|
pcGridAdapter.addComputer(new ComputerObject(details));
|
||||||
|
|
||||||
|
// Remove the "Discovery in progress" view
|
||||||
|
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the view that the data has changed
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ComputerObject {
|
||||||
|
public ComputerDetails details;
|
||||||
|
|
||||||
|
public ComputerObject(ComputerDetails details) {
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return details.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.AudioTrack;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
|
||||||
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
|
public static final int FRAME_SIZE = 960;
|
||||||
|
|
||||||
|
private AudioTrack track;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||||
|
int channelConfig;
|
||||||
|
int bufferSize;
|
||||||
|
|
||||||
|
switch (channelCount)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're not supposed to request less than the minimum
|
||||||
|
// buffer size for our buffer, but it appears that we can
|
||||||
|
// do this on many devices and it lowers audio latency.
|
||||||
|
// We'll try the small buffer size first and if it fails,
|
||||||
|
// use the recommended larger buffer size.
|
||||||
|
try {
|
||||||
|
// Buffer two frames of audio if possible
|
||||||
|
bufferSize = FRAME_SIZE * 2;
|
||||||
|
|
||||||
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
|
sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
track.play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try to release the AudioTrack if we got far enough
|
||||||
|
try {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Now try the larger buffer size
|
||||||
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
|
FRAME_SIZE * 2);
|
||||||
|
|
||||||
|
// Round to next frame
|
||||||
|
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
||||||
|
|
||||||
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
|
sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
track.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||||
|
track.write(audioData, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void streamClosing() {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCapabilities() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,722 @@
|
|||||||
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
|
public class ControllerHandler {
|
||||||
|
private short inputMap = 0x0000;
|
||||||
|
private byte leftTrigger = 0x00;
|
||||||
|
private byte rightTrigger = 0x00;
|
||||||
|
private short rightStickX = 0x0000;
|
||||||
|
private short rightStickY = 0x0000;
|
||||||
|
private short leftStickX = 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 NvConnection conn;
|
||||||
|
private double stickDeadzone;
|
||||||
|
private final ControllerMapping defaultMapping = new ControllerMapping();
|
||||||
|
|
||||||
|
public ControllerHandler(NvConnection conn, int deadzonePercentage) {
|
||||||
|
this.conn = conn;
|
||||||
|
|
||||||
|
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||||
|
// is required for controller batching support to work.
|
||||||
|
deadzonePercentage = 10;
|
||||||
|
|
||||||
|
// 1% is the lowest possible deadzone we support
|
||||||
|
if (deadzonePercentage <= 0) {
|
||||||
|
deadzonePercentage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stickDeadzone = (double)deadzonePercentage / 100.0;
|
||||||
|
|
||||||
|
// Initialize the default mapping for events with no device
|
||||||
|
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
|
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
|
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||||
|
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
|
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
|
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
|
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||||
|
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) {
|
||||||
|
ControllerMapping mapping = new ControllerMapping();
|
||||||
|
String devName = dev.getName();
|
||||||
|
|
||||||
|
LimeLog.info("Creating controller mapping for device: "+devName);
|
||||||
|
|
||||||
|
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
|
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
|
|
||||||
|
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||||
|
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||||
|
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||||
|
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||||
|
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||||
|
{
|
||||||
|
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||||
|
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
|
||||||
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
|
||||||
|
}
|
||||||
|
else if (brakeRange != null && gasRange != null)
|
||||||
|
{
|
||||||
|
// Others use GAS and BRAKE (like Moga)
|
||||||
|
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||||
|
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
|
if (rxRange != null && ryRange != null && devName != null) {
|
||||||
|
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||||
|
// Xbox controllers use RX and RY for right stick
|
||||||
|
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||||
|
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||||
|
|
||||||
|
// Xbox controllers use Z and RZ for triggers
|
||||||
|
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||||
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||||
|
mapping.triggersIdleNegative = true;
|
||||||
|
mapping.isXboxController = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// DS4 controller uses RX and RY for triggers
|
||||||
|
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||||
|
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||||
|
mapping.triggersIdleNegative = true;
|
||||||
|
|
||||||
|
mapping.isDualShock4 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||||
|
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
|
||||||
|
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
|
||||||
|
|
||||||
|
// Most other controllers use Z and RZ for the right stick
|
||||||
|
if (zRange != null && rzRange != null) {
|
||||||
|
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
|
||||||
|
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||||
|
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||||
|
|
||||||
|
// Try RX and RY now
|
||||||
|
if (rxRange != null && ryRange != null) {
|
||||||
|
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||||
|
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some devices have "hats" for d-pads
|
||||||
|
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
|
||||||
|
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
|
||||||
|
if (hatXRange != null && hatYRange != null) {
|
||||||
|
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||||
|
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
|
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
|
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
|
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
|
||||||
|
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis);
|
||||||
|
|
||||||
|
// It's important to have a valid deadzone so controller packet batching works properly
|
||||||
|
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
|
||||||
|
|
||||||
|
// For triggers without (valid) deadzones, we'll use 10%
|
||||||
|
if (mapping.triggerDeadzone <= 0.02 ||
|
||||||
|
mapping.triggerDeadzone > 0.30)
|
||||||
|
{
|
||||||
|
mapping.triggerDeadzone = 0.1f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the Nexus Player (and probably other ATV devices), we should
|
||||||
|
// use the back button as start since it doesn't have a start/menu button
|
||||||
|
// on the controller
|
||||||
|
if (devName != null && devName.contains("ASUS Gamepad")) {
|
||||||
|
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
|
||||||
|
// is Android 5.0 anyway
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||||
|
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
|
||||||
|
if (!hasStartKey[0] && !hasStartKey[1]) {
|
||||||
|
mapping.backIsStart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
||||||
|
// Unknown devices use the default mapping
|
||||||
|
if (dev == null) {
|
||||||
|
return defaultMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
String descriptor = dev.getDescriptor();
|
||||||
|
|
||||||
|
// Return the existing mapping if it exists
|
||||||
|
ControllerMapping mapping = mappings.get(descriptor);
|
||||||
|
if (mapping != null) {
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise create a new mapping
|
||||||
|
mapping = createMappingForDevice(dev);
|
||||||
|
mappings.put(descriptor, mapping);
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControllerInputPacket() {
|
||||||
|
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
||||||
|
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||||
|
if (mapping.isDualShock4) {
|
||||||
|
switch (event.getKeyCode()) {
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_Z:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_C:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_A;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_A:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_X;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
|
||||||
|
// These are duplicate trigger events
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
|
switch (event.getKeyCode()) {
|
||||||
|
// These are duplicate dpad events for hat input
|
||||||
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||||
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Past here we can fixup the keycode and potentially trigger
|
||||||
|
// another special case so we need to remember what keycode we're using
|
||||||
|
int keyCode = event.getKeyCode();
|
||||||
|
|
||||||
|
// This is a hack for (at least) the "Tablet Remote" app
|
||||||
|
// which sends BACK with META_ALT_ON instead of KEYCODE_BUTTON_B
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK &&
|
||||||
|
!event.hasNoModifiers() &&
|
||||||
|
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0)
|
||||||
|
{
|
||||||
|
keyCode = KeyEvent.KEYCODE_BUTTON_B;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
|
||||||
|
keyCode == KeyEvent.KEYCODE_MENU) {
|
||||||
|
// Ensure that we never use back as start if we have a real start
|
||||||
|
mapping.backIsStart = false;
|
||||||
|
}
|
||||||
|
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
// Emulate the start button with back
|
||||||
|
return KeyEvent.KEYCODE_BUTTON_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
/*
|
||||||
|
FIXME: We're not normalizing here because we let the computer handle the deadzones.
|
||||||
|
Normalizing can make the deadzones larger than they should be after the computer also
|
||||||
|
evaluates the deadzone
|
||||||
|
|
||||||
|
// Scale the input based on the distance from the deadzone
|
||||||
|
inputVector.getNormalized(normalizedInputVector);
|
||||||
|
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
*/
|
||||||
|
|
||||||
|
return inputVector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
|
||||||
|
float rsY, float lt, float rt, float hatX, float hatY) {
|
||||||
|
|
||||||
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
|
Vector2d leftStickVector = handleDeadZone(lsX, lsY, mapping.leftStickDeadzoneRadius);
|
||||||
|
|
||||||
|
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
|
||||||
|
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
|
Vector2d rightStickVector = handleDeadZone(rsX, rsY, mapping.rightStickDeadzoneRadius);
|
||||||
|
|
||||||
|
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
|
||||||
|
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
|
if (mapping.triggersIdleNegative) {
|
||||||
|
lt = (lt + 1) / 2;
|
||||||
|
rt = (rt + 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lt <= mapping.triggerDeadzone) {
|
||||||
|
lt = 0;
|
||||||
|
}
|
||||||
|
if (rt <= mapping.triggerDeadzone) {
|
||||||
|
rt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
leftTrigger = (byte)(lt * 0xFF);
|
||||||
|
rightTrigger = (byte)(rt * 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
|
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||||
|
if (hatX < -0.5) {
|
||||||
|
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
}
|
||||||
|
else if (hatX > 0.5) {
|
||||||
|
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||||
|
if (hatY < -0.5) {
|
||||||
|
inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
}
|
||||||
|
else if (hatY > 0.5) {
|
||||||
|
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendControllerInputPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleMotionEvent(MotionEvent event) {
|
||||||
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
|
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
||||||
|
|
||||||
|
// Replay the full history before getting the current values
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
|
lsX = event.getHistoricalAxisValue(mapping.leftStickXAxis, i);
|
||||||
|
lsY = event.getHistoricalAxisValue(mapping.leftStickYAxis, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
|
rsX = event.getHistoricalAxisValue(mapping.rightStickXAxis, i);
|
||||||
|
rsY = event.getHistoricalAxisValue(mapping.rightStickYAxis, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
|
lt = event.getHistoricalAxisValue(mapping.leftTriggerAxis, i);
|
||||||
|
rt = event.getHistoricalAxisValue(mapping.rightTriggerAxis, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
|
hatX = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, i);
|
||||||
|
hatY = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now handle the current set of values
|
||||||
|
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||||
|
lsX = event.getAxisValue(mapping.leftStickXAxis);
|
||||||
|
lsY = event.getAxisValue(mapping.leftStickYAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||||
|
rsX = event.getAxisValue(mapping.rightStickXAxis);
|
||||||
|
rsY = event.getAxisValue(mapping.rightStickYAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
|
||||||
|
lt = event.getAxisValue(mapping.leftTriggerAxis);
|
||||||
|
rt = event.getAxisValue(mapping.rightTriggerAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||||
|
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
|
||||||
|
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleButtonUp(KeyEvent event) {
|
||||||
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
|
|
||||||
|
int keyCode = handleRemapping(mapping, event);
|
||||||
|
if (keyCode == 0) {
|
||||||
|
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) {
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
|
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
|
case KeyEvent.KEYCODE_MENU:
|
||||||
|
inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BACK:
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
|
inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
|
inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
|
inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
|
inputMap &= ~ControllerPacket.UP_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
|
inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
|
inputMap &= ~ControllerPacket.B_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_A:
|
||||||
|
inputMap &= ~ControllerPacket.A_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
|
inputMap &= ~ControllerPacket.X_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
|
inputMap &= ~ControllerPacket.Y_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
|
inputMap &= ~ControllerPacket.LB_FLAG;
|
||||||
|
lastLbUpTime = SystemClock.uptimeMillis();
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
|
inputMap &= ~ControllerPacket.RB_FLAG;
|
||||||
|
lastRbUpTime = SystemClock.uptimeMillis();
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
|
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||||
|
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
|
leftTrigger = 0;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
|
rightTrigger = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're emulating the select button
|
||||||
|
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
|
||||||
|
{
|
||||||
|
// 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();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleButtonDown(KeyEvent event) {
|
||||||
|
ControllerMapping mapping = getMappingForDevice(event.getDevice());
|
||||||
|
|
||||||
|
int keyCode = handleRemapping(mapping, event);
|
||||||
|
if (keyCode == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_MODE:
|
||||||
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_START:
|
||||||
|
case KeyEvent.KEYCODE_MENU:
|
||||||
|
inputMap |= ControllerPacket.PLAY_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BACK:
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||||
|
inputMap |= ControllerPacket.BACK_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
|
inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
|
inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
|
inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
|
inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_B:
|
||||||
|
inputMap |= ControllerPacket.B_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_A:
|
||||||
|
inputMap |= ControllerPacket.A_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_X:
|
||||||
|
inputMap |= ControllerPacket.X_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||||
|
inputMap |= ControllerPacket.Y_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||||
|
inputMap |= ControllerPacket.LB_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||||
|
inputMap |= ControllerPacket.RB_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_THUMBL:
|
||||||
|
inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_THUMBR:
|
||||||
|
inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
|
leftTrigger = (byte)0xFF;
|
||||||
|
break;
|
||||||
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
|
rightTrigger = (byte)0xFF;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start+LB acts like select for controllers with one button
|
||||||
|
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.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||||
|
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||||
|
|
||||||
|
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a new input packet if this is the first instance of a button down event
|
||||||
|
// or anytime if we're emulating a button
|
||||||
|
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
|
||||||
|
sendControllerInputPacket();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControllerMapping {
|
||||||
|
public int leftStickXAxis = -1;
|
||||||
|
public int leftStickYAxis = -1;
|
||||||
|
public float leftStickDeadzoneRadius;
|
||||||
|
|
||||||
|
public int rightStickXAxis = -1;
|
||||||
|
public int rightStickYAxis = -1;
|
||||||
|
public float rightStickDeadzoneRadius;
|
||||||
|
|
||||||
|
public int leftTriggerAxis = -1;
|
||||||
|
public int rightTriggerAxis = -1;
|
||||||
|
public boolean triggersIdleNegative;
|
||||||
|
public float triggerDeadzone;
|
||||||
|
|
||||||
|
public int hatXAxis = -1;
|
||||||
|
public int hatYAxis = -1;
|
||||||
|
|
||||||
|
public boolean isDualShock4;
|
||||||
|
public boolean isXboxController;
|
||||||
|
public boolean backIsStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.KeycodeTranslator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
|
* @author Diego Waxemberg
|
||||||
|
* @author Cameron Gutman
|
||||||
|
*/
|
||||||
|
public class KeyboardTranslator extends KeycodeTranslator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GFE's prefix for every key code
|
||||||
|
*/
|
||||||
|
public static final short KEY_PREFIX = (short) 0x80;
|
||||||
|
|
||||||
|
public static final int VK_0 = 48;
|
||||||
|
public static final int VK_9 = 57;
|
||||||
|
public static final int VK_A = 65;
|
||||||
|
public static final int VK_Z = 90;
|
||||||
|
public static final int VK_ALT = 18;
|
||||||
|
public static final int VK_NUMPAD0 = 96;
|
||||||
|
public static final int VK_BACK_SLASH = 92;
|
||||||
|
public static final int VK_CAPS_LOCK = 20;
|
||||||
|
public static final int VK_CLEAR = 12;
|
||||||
|
public static final int VK_COMMA = 44;
|
||||||
|
public static final int VK_CONTROL = 17;
|
||||||
|
public static final int VK_BACK_SPACE = 8;
|
||||||
|
public static final int VK_EQUALS = 61;
|
||||||
|
public static final int VK_ESCAPE = 27;
|
||||||
|
public static final int VK_F1 = 112;
|
||||||
|
public static final int VK_PERIOD = 46;
|
||||||
|
public static final int VK_INSERT = 155;
|
||||||
|
public static final int VK_OPEN_BRACKET = 91;
|
||||||
|
public static final int VK_WINDOWS = 524;
|
||||||
|
public static final int VK_MINUS = 45;
|
||||||
|
public static final int VK_END = 35;
|
||||||
|
public static final int VK_HOME = 36;
|
||||||
|
public static final int VK_NUM_LOCK = 144;
|
||||||
|
public static final int VK_PAGE_UP = 33;
|
||||||
|
public static final int VK_PAGE_DOWN = 34;
|
||||||
|
public static final int VK_PLUS = 521;
|
||||||
|
public static final int VK_CLOSE_BRACKET = 93;
|
||||||
|
public static final int VK_SCROLL_LOCK = 145;
|
||||||
|
public static final int VK_SEMICOLON = 59;
|
||||||
|
public static final int VK_SHIFT = 16;
|
||||||
|
public static final int VK_SLASH = 47;
|
||||||
|
public static final int VK_SPACE = 32;
|
||||||
|
public static final int VK_PRINTSCREEN = 154;
|
||||||
|
public static final int VK_TAB = 9;
|
||||||
|
public static final int VK_LEFT = 37;
|
||||||
|
public static final int VK_RIGHT = 39;
|
||||||
|
public static final int VK_UP = 38;
|
||||||
|
public static final int VK_DOWN = 40;
|
||||||
|
public static final int VK_BACK_QUOTE = 192;
|
||||||
|
public static final int VK_QUOTE = 222;
|
||||||
|
public static final int VK_PAUSE = 19;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new translator for the specified connection
|
||||||
|
* @param conn the connection to which the translated codes are sent
|
||||||
|
*/
|
||||||
|
public KeyboardTranslator(NvConnection conn) {
|
||||||
|
super(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the given keycode and returns the GFE keycode
|
||||||
|
* @param keycode the code to be translated
|
||||||
|
* @return a GFE keycode for the given keycode
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public short translate(int keycode) {
|
||||||
|
int translated;
|
||||||
|
|
||||||
|
/* There seems to be no clean mapping between Android key codes
|
||||||
|
* and what Nvidia sends over the wire. If someone finds one,
|
||||||
|
* I'll happily delete this code :)
|
||||||
|
*/
|
||||||
|
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||||
|
keycode <= KeyEvent.KEYCODE_9) {
|
||||||
|
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||||
|
}
|
||||||
|
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||||
|
keycode <= KeyEvent.KEYCODE_Z) {
|
||||||
|
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||||
|
}
|
||||||
|
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||||
|
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||||
|
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||||
|
}
|
||||||
|
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||||
|
keycode <= KeyEvent.KEYCODE_F12) {
|
||||||
|
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch (keycode) {
|
||||||
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
|
translated = VK_ALT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
|
translated = 0xdc;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||||
|
translated = VK_CAPS_LOCK;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_CLEAR:
|
||||||
|
translated = VK_CLEAR;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_COMMA:
|
||||||
|
translated = 0xbc;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
|
translated = VK_CONTROL;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_DEL:
|
||||||
|
translated = VK_BACK_SPACE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_ENTER:
|
||||||
|
translated = 0x0d;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_EQUALS:
|
||||||
|
translated = 0xbb;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_ESCAPE:
|
||||||
|
translated = VK_ESCAPE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
|
// Nvidia maps period to delete
|
||||||
|
translated = VK_PERIOD;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
|
translated = -1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||||
|
translated = 0xdb;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_META_LEFT:
|
||||||
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
|
translated = VK_WINDOWS;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
|
translated = 0xbd;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_MOVE_END:
|
||||||
|
translated = VK_END;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||||
|
translated = VK_HOME;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||||
|
translated = VK_NUM_LOCK;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||||
|
translated = VK_PAGE_DOWN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_PAGE_UP:
|
||||||
|
translated = VK_PAGE_UP;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_PERIOD:
|
||||||
|
translated = 0xbe;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||||
|
translated = 0xdd;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||||
|
translated = VK_SCROLL_LOCK;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_SEMICOLON:
|
||||||
|
translated = 0xba;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
|
translated = VK_SHIFT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
|
translated = 0xbf;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_SPACE:
|
||||||
|
translated = VK_SPACE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_SYSRQ:
|
||||||
|
// Android defines this as SysRq/PrntScrn
|
||||||
|
translated = VK_PRINTSCREEN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_TAB:
|
||||||
|
translated = VK_TAB;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
|
translated = VK_LEFT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
|
translated = VK_RIGHT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
|
translated = VK_UP;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
|
translated = VK_DOWN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_GRAVE:
|
||||||
|
translated = VK_BACK_QUOTE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||||
|
translated = 0xde;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_BREAK:
|
||||||
|
translated = VK_PAUSE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
System.out.println("No key for "+keycode);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (short) ((KEY_PREFIX << 8) | translated);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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 double xFactor, yFactor;
|
||||||
|
|
||||||
|
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||||
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
|
||||||
|
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||||
|
{
|
||||||
|
this.conn = conn;
|
||||||
|
this.actionIndex = actionIndex;
|
||||||
|
this.xFactor = xFactor;
|
||||||
|
this.yFactor = yFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActionIndex()
|
||||||
|
{
|
||||||
|
return 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) {
|
||||||
|
int deltaX = eventX - lastTouchX;
|
||||||
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
|
deltaX = (int)Math.round((double)deltaX * xFactor);
|
||||||
|
deltaY = (int)Math.round((double)deltaY * yFactor);
|
||||||
|
|
||||||
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTouchX = eventX;
|
||||||
|
lastTouchY = eventY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
-27
@@ -6,30 +6,39 @@ import java.io.FileReader;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.os.Build;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
|
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
private Thread rendererThread;
|
private Thread rendererThread, decoderThread;
|
||||||
private int targetFps;
|
private int targetFps;
|
||||||
|
|
||||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||||
private ByteBuffer decoderBuffer;
|
private ByteBuffer decoderBuffer;
|
||||||
|
|
||||||
// Only sleep if the difference is above this value
|
// Only sleep if the difference is above this value
|
||||||
private static final int WAIT_CEILING_MS = 8;
|
private static final int WAIT_CEILING_MS = 5;
|
||||||
|
|
||||||
private static final int LOW_PERF = 1;
|
private static final int LOW_PERF = 1;
|
||||||
private static final int MED_PERF = 2;
|
private static final int MED_PERF = 2;
|
||||||
private static final int HIGH_PERF = 3;
|
private static final int HIGH_PERF = 3;
|
||||||
|
|
||||||
|
private int totalFrames;
|
||||||
|
private 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;
|
||||||
@@ -47,8 +56,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;
|
||||||
}
|
}
|
||||||
@@ -78,10 +91,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;
|
||||||
@@ -94,9 +107,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
case LOW_PERF:
|
case LOW_PERF:
|
||||||
// Disable the loop filter for performance reasons
|
// Disable the loop filter for performance reasons
|
||||||
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
|
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
|
||||||
AvcDecoder.FAST_BILINEAR_FILTERING |
|
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// Use plenty of threads to try to utilize the CPU as best we can
|
// Use plenty of threads to try to utilize the CPU as best we can
|
||||||
threadCount = cpuCount - 1;
|
threadCount = cpuCount - 1;
|
||||||
@@ -104,8 +115,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
case MED_PERF:
|
case MED_PERF:
|
||||||
avcFlags = AvcDecoder.BILINEAR_FILTERING |
|
avcFlags = AvcDecoder.BILINEAR_FILTERING;
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// Only use 2 threads to minimize frame processing latency
|
// Only use 2 threads to minimize frame processing latency
|
||||||
threadCount = 2;
|
threadCount = 2;
|
||||||
@@ -120,38 +130,64 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// Disable the non-compliant speed optimizations
|
// Disable the non-compliant speed optimizations
|
||||||
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
||||||
|
|
||||||
System.out.println("Using high quality decoding");
|
LimeLog.info("Using high quality decoding");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SurfaceHolder sh = (SurfaceHolder)renderTarget;
|
||||||
|
sh.setFormat(PixelFormat.RGBX_8888);
|
||||||
|
|
||||||
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||||
}
|
}
|
||||||
|
|
||||||
AvcDecoder.setRenderTarget(((SurfaceHolder)renderTarget).getSurface());
|
AvcDecoder.setRenderTarget(sh.getSurface());
|
||||||
|
|
||||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||||
|
|
||||||
System.out.println("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) {
|
||||||
|
decoderThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
DecodeUnit du;
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
du = depacketizer.takeNextDecodeUnit();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitDecodeUnit(du);
|
||||||
|
depacketizer.freeDecodeUnit(du);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
decoderThread.setName("Video - Decoder (CPU)");
|
||||||
|
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
|
||||||
|
decoderThread.start();
|
||||||
|
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
|
DecodeUnit du;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
if (diff > WAIT_CEILING_MS) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(diff);
|
Thread.sleep(diff - WAIT_CEILING_MS);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||||
@@ -160,7 +196,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) {
|
||||||
@@ -170,10 +208,14 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
rendererThread.interrupt();
|
rendererThread.interrupt();
|
||||||
|
decoderThread.interrupt();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException e) { }
|
} catch (InterruptedException e) { }
|
||||||
|
try {
|
||||||
|
decoderThread.join();
|
||||||
|
} catch (InterruptedException e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -181,8 +223,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
|
||||||
@@ -205,6 +246,41 @@ 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
|
||||||
|
public int getCapabilities() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (totalFrames == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)(totalTimeMs / totalFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
return "CPU decoding";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
|
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
|
private EnhancedDecoderRenderer 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getDecoderName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
|
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
public abstract String getDecoderName();
|
||||||
|
}
|
||||||
@@ -0,0 +1,600 @@
|
|||||||
|
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 extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
|
private MediaCodec videoDecoder;
|
||||||
|
private Thread rendererThread;
|
||||||
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
|
private VideoDepacketizer depacketizer;
|
||||||
|
private boolean adaptivePlayback;
|
||||||
|
private int initialWidth, initialHeight;
|
||||||
|
|
||||||
|
private boolean needsBaselineSpsHack;
|
||||||
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
|
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);
|
||||||
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
||||||
|
isExynos4 = MediaCodecHelper.isExynos4Device();
|
||||||
|
if (needsSpsBitstreamFixup) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
||||||
|
}
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
|
||||||
|
}
|
||||||
|
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 >= 0 && 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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to hack this SPS to say we're baseline, do so now
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Hacking SPS to baseline");
|
||||||
|
sps.profile_idc = 66;
|
||||||
|
savedSps = sps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the annex B header
|
||||||
|
buf.put(header.data, header.offset, 5);
|
||||||
|
|
||||||
|
// 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++;
|
||||||
|
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Saw PPS; disabling SPS hack");
|
||||||
|
needsBaselineSpsHack = false;
|
||||||
|
|
||||||
|
// Give the decoder the SPS again with the proper profile now
|
||||||
|
needsSpsReplay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy data from our buffer list into the input buffer
|
||||||
|
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
||||||
|
{
|
||||||
|
buf.put(desc.data, desc.offset, desc.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
queueInputBuffer(inputBufferIndex,
|
||||||
|
0, decodeUnit.getDataLength(),
|
||||||
|
timestampUs, codecFlags);
|
||||||
|
|
||||||
|
depacketizer.freeDecodeUnit(decodeUnit);
|
||||||
|
|
||||||
|
if (needsSpsReplay) {
|
||||||
|
replaySps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaySps() {
|
||||||
|
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
||||||
|
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
||||||
|
|
||||||
|
inputBuffer.clear();
|
||||||
|
|
||||||
|
// Write the Annex B header
|
||||||
|
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||||
|
|
||||||
|
// Switch the H264 profile back to high
|
||||||
|
savedSps.profile_idc = 100;
|
||||||
|
|
||||||
|
// Write the SPS data
|
||||||
|
savedSps.write(inputBuffer);
|
||||||
|
|
||||||
|
// No need for the SPS anymore
|
||||||
|
savedSps = null;
|
||||||
|
|
||||||
|
// Queue the new SPS
|
||||||
|
queueInputBuffer(inputIndex,
|
||||||
|
0, inputBuffer.position(),
|
||||||
|
System.currentTimeMillis() * 1000,
|
||||||
|
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||||
|
|
||||||
|
LimeLog.info("SPS replay complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCapabilities() {
|
||||||
|
return adaptivePlayback ?
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
return decoderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,312 @@
|
|||||||
|
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;
|
||||||
|
public static final List<String> baselineProfileHackPrefixes;
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||||
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
/*
|
||||||
|
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||||
|
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||||
|
|
||||||
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
return isDecoderInList(baselineProfileHackPrefixes, 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,169 @@
|
|||||||
|
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;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, skip the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
details.macAddress == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
computerList.add(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, delete the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
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,451 @@
|
|||||||
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
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 = 3000;
|
||||||
|
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 final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||||
|
private ComputerManagerListener listener = null;
|
||||||
|
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
|
private boolean pollingActive = false;
|
||||||
|
|
||||||
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
|
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.isEmpty();
|
||||||
|
|
||||||
|
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() && pollingActive) {
|
||||||
|
// Check if this poll has modified the details
|
||||||
|
runPoll(details);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Polling is active
|
||||||
|
pollingActive = true;
|
||||||
|
|
||||||
|
// Set the listener
|
||||||
|
ComputerManagerService.this.listener = listener;
|
||||||
|
|
||||||
|
// Start mDNS autodiscovery too
|
||||||
|
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||||
|
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
// This polling thread might already be there
|
||||||
|
if (tuple.thread == null) {
|
||||||
|
// Report this computer initially
|
||||||
|
listener.notifyComputerUpdated(tuple.computer);
|
||||||
|
|
||||||
|
tuple.thread = createPollingThread(tuple.computer);
|
||||||
|
tuple.thread.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
|
||||||
|
pollingActive = false;
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
// Interrupt and remove the thread
|
||||||
|
tuple.thread.interrupt();
|
||||||
|
tuple.thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
fakeDetails.name = "";
|
||||||
|
|
||||||
|
addTuple(fakeDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTuple(ComputerDetails details) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
// Check if this is the same computer
|
||||||
|
if (tuple.computer == details ||
|
||||||
|
tuple.computer.localIp.equals(details.localIp) ||
|
||||||
|
tuple.computer.remoteIp.equals(details.remoteIp) ||
|
||||||
|
tuple.computer.name.equals(details.name)) {
|
||||||
|
|
||||||
|
// Start a polling thread if polling is active
|
||||||
|
if (pollingActive && tuple.thread == null) {
|
||||||
|
tuple.thread = createPollingThread(details);
|
||||||
|
tuple.thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found an entry so we're done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, we didn't find an entry
|
||||||
|
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
|
||||||
|
pollingTuples.add(tuple);
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
tuple.thread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addComputerBlocking(InetAddress addr) {
|
||||||
|
// Setup a placeholder
|
||||||
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
|
fakeDetails.localIp = addr;
|
||||||
|
fakeDetails.remoteIp = addr;
|
||||||
|
fakeDetails.name = "";
|
||||||
|
|
||||||
|
// Block while we try to fill the details
|
||||||
|
runPoll(fakeDetails);
|
||||||
|
|
||||||
|
// If the machine is reachable, it was successful
|
||||||
|
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||||
|
// Start a polling thread for this machine
|
||||||
|
addTuple(fakeDetails);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeComputer(String name) {
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove it from the database
|
||||||
|
dbManager.deleteComputer(name);
|
||||||
|
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
// Remove the computer from the computer list
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (tuple.computer.name.equals(name)) {
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
// Interrupt the thread on this entry
|
||||||
|
tuple.thread.interrupt();
|
||||||
|
}
|
||||||
|
pollingTuples.remove(tuple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||||
|
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Always try local first to avoid potential UDP issues when
|
||||||
|
// attempting to stream via the router's external IP address
|
||||||
|
// behind its NAT
|
||||||
|
return pollComputer(details, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we're already reached a machine via a particular IP address,
|
||||||
|
// always try that one first
|
||||||
|
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
// Bind to the discovery service
|
||||||
|
bindService(new Intent(this, DiscoveryService.class),
|
||||||
|
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
// Lookup or generate this device's UID
|
||||||
|
idManager = new IdentityManager(this);
|
||||||
|
|
||||||
|
// Initialize the DB
|
||||||
|
dbManager = new ComputerDatabaseManager(this);
|
||||||
|
dbRefCount.set(1);
|
||||||
|
|
||||||
|
// Grab known machines into our computer list
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
|
// Add this computer without a thread
|
||||||
|
pollingTuples.add(new PollingTuple(computer, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollingTuple {
|
||||||
|
public Thread thread;
|
||||||
|
public ComputerDetails computer;
|
||||||
|
|
||||||
|
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.thread = thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.koushikdutta.async.future.FutureCallback;
|
||||||
|
import com.koushikdutta.ion.Ion;
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509KeyManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
|
||||||
|
private InetAddress address;
|
||||||
|
private String uniqueId;
|
||||||
|
private LimelightCryptoProvider cryptoProvider;
|
||||||
|
private SSLContext sslContext;
|
||||||
|
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||||
|
|
||||||
|
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||||
|
super(context, R.layout.app_grid_item, R.drawable.image_loading);
|
||||||
|
|
||||||
|
this.address = address;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
|
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||||
|
|
||||||
|
sslContext = SSLContext.getInstance("SSL");
|
||||||
|
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||||
|
}
|
||||||
|
|
||||||
|
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||||
|
new X509TrustManager() {
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
}};
|
||||||
|
|
||||||
|
KeyManager[] ourKeyman = new KeyManager[] {
|
||||||
|
new X509KeyManager() {
|
||||||
|
public String chooseClientAlias(String[] keyTypes,
|
||||||
|
Principal[] issuers, Socket socket) {
|
||||||
|
return "Limelight-RSA";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||||
|
Socket socket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate[] getCertificateChain(String alias) {
|
||||||
|
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateKey getPrivateKey(String alias) {
|
||||||
|
return cryptoProvider.getClientPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore differences between given hostname and certificate hostname
|
||||||
|
HostnameVerifier hv = new HostnameVerifier() {
|
||||||
|
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
public void addApp(AppView.AppObject app) {
|
||||||
|
itemList.add(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abortPendingRequests() {
|
||||||
|
HashMap<ImageView, Future> tempMap;
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Copy the pending requests under a lock
|
||||||
|
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Future f : tempMap.values()) {
|
||||||
|
f.cancel(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Remove cancelled requests
|
||||||
|
for (ImageView v : tempMap.keySet()) {
|
||||||
|
pendingRequests.remove(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) {
|
||||||
|
|
||||||
|
// Set SSL contexts correctly to allow us to authenticate
|
||||||
|
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||||
|
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||||
|
|
||||||
|
// Set off the deferred image load
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
Future f = Ion.with(imgView)
|
||||||
|
.placeholder(defaultImageRes)
|
||||||
|
.error(defaultImageRes)
|
||||||
|
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||||
|
obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
|
||||||
|
.setCallback(new FutureCallback<ImageView>() {
|
||||||
|
@Override
|
||||||
|
public void onCompleted(Exception e, ImageView result) {
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
pendingRequests.remove(imgView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendingRequests.put(imgView, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
|
||||||
|
// Select the text view so it starts marquee mode
|
||||||
|
txtView.setSelected(true);
|
||||||
|
|
||||||
|
// Return false to use the app's toString method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||||
|
if (obj.app.getIsRunning()) {
|
||||||
|
// Show the play button overlay
|
||||||
|
overlayView.setImageResource(R.drawable.play);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.BaseAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
|
protected Context context;
|
||||||
|
protected int defaultImageRes;
|
||||||
|
protected int layoutId;
|
||||||
|
protected ArrayList<T> itemList = new ArrayList<T>();
|
||||||
|
protected LayoutInflater inflater;
|
||||||
|
|
||||||
|
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||||
|
this.context = context;
|
||||||
|
this.layoutId = layoutId;
|
||||||
|
this.defaultImageRes = defaultImageRes;
|
||||||
|
|
||||||
|
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
itemList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return itemList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getItem(int i) {
|
||||||
|
return itemList.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int i) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean populateImageView(ImageView imgView, T obj);
|
||||||
|
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||||
|
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||||
|
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||||
|
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||||
|
|
||||||
|
if (!populateImageView(imgView, itemList.get(i))) {
|
||||||
|
imgView.setImageResource(defaultImageRes);
|
||||||
|
}
|
||||||
|
if (!populateTextView(txtView, itemList.get(i))) {
|
||||||
|
txtView.setText(itemList.get(i).toString());
|
||||||
|
}
|
||||||
|
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
||||||
|
overlayView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.PcView;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
|
public PcGridAdapter(Context context) {
|
||||||
|
super(context, R.layout.pc_grid_item, R.drawable.computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
|
itemList.add(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeComputer(PcView.ComputerObject computer) {
|
||||||
|
return itemList.remove(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
imgView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
imgView.setAlpha(0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false to use the default drawable
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
txtView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
txtView.setAlpha(0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false to use the computer's toString method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
|
// Still refreshing this PC so display the overlay
|
||||||
|
overlayView.setImageResource(R.drawable.image_loading);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
-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,163 @@
|
|||||||
|
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 com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
public class AddComputerManually extends Activity {
|
||||||
|
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;
|
||||||
|
|
||||||
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
|
|
||||||
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
|
finish = true;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
|
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();
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||||
|
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||||
|
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||||
|
if (hostText.getText().length() == 0) {
|
||||||
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
computersToAdd.add(hostText.getText().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind to the ComputerManager service
|
||||||
|
bindService(new Intent(AddComputerManually.this,
|
||||||
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
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 String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||||
|
|
||||||
|
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;
|
||||||
|
private static final int DEFAULT_DEADZONE = 15;
|
||||||
|
|
||||||
|
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 int deadzonePercentage;
|
||||||
|
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);
|
||||||
|
|
||||||
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
|
// Checkbox preferences
|
||||||
|
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
||||||
|
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||||
|
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,165 @@
|
|||||||
|
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 valueText;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
private String dialogMessage, suffix;
|
||||||
|
private int defaultValue, maxValue, minValue, 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, min, and max seekbar values
|
||||||
|
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
|
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||||
|
minValue = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateDialogView() {
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params;
|
||||||
|
LinearLayout layout = new LinearLayout(context);
|
||||||
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
layout.setPadding(6, 6, 6, 6);
|
||||||
|
|
||||||
|
TextView 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.MATCH_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) {
|
||||||
|
if (value < minValue) {
|
||||||
|
seekBar.setProgress(minValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String t = String.valueOf(value);
|
||||||
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_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(seekBar.getProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
getDialog().dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
public class StreamSettings extends Activity {
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_stream_settings);
|
||||||
|
getFragmentManager().beginTransaction().replace(
|
||||||
|
R.id.stream_settings, new SettingsFragment()
|
||||||
|
).commit();
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
|
||||||
|
// Add a listener to the FPS and resolution preference
|
||||||
|
// so the bitrate can be auto-adjusted
|
||||||
|
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||||
|
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
|
String valueStr = (String) newValue;
|
||||||
|
|
||||||
|
// Write the new bitrate value
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||||
|
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
// Allow the original preference change to take place
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.UiModeManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public class UiHelper {
|
||||||
|
|
||||||
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
|
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||||
|
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||||
|
|
||||||
|
public static void notifyNewRootView(Activity activity)
|
||||||
|
{
|
||||||
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
|
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||||
|
|
||||||
|
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
||||||
|
{
|
||||||
|
// Increase view padding on TVs
|
||||||
|
float scale = activity.getResources().getDisplayMetrics().density;
|
||||||
|
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
||||||
|
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
||||||
|
|
||||||
|
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||||
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -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.
|
||||||
File renamed without changes.
File renamed without changes.
-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,
|
||||||
File renamed without changes.
File renamed without changes.
+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;
|
||||||
File renamed without changes.
+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
|
||||||
* @{
|
* @{
|
||||||
File renamed without changes.
+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
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 574 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user