mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-22 16:17:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8086c3d46b | ||
|
|
928fca843f | ||
|
|
25d74785d0 | ||
|
|
e12a8e7946 | ||
|
|
b14f2ce219 | ||
|
|
d31be3d64e | ||
|
|
0704f2aaf6 | ||
|
|
832e52ac74 | ||
|
|
f5444551b2 | ||
|
|
3143797b55 | ||
|
|
cc9b1aeaab | ||
|
|
3d177e97e4 | ||
|
|
6c3aaedc83 | ||
|
|
bf84ebef6d | ||
|
|
8991b29329 | ||
|
|
fa84575be5 | ||
|
|
0432d5725b | ||
|
|
8e7b144339 | ||
|
|
fc629db653 | ||
|
|
d5863e1bef | ||
|
|
c2c3a6b37c | ||
|
|
e701699dea | ||
|
|
17179bd027 | ||
|
|
b2f210700d | ||
|
|
f0e85c4c53 | ||
|
|
92f8425ace | ||
|
|
6ad001e8be | ||
|
|
b6e4d5528b | ||
|
|
0f0b83badc | ||
|
|
453fbb5f58 | ||
|
|
e7dc3a4c11 | ||
|
|
d68b2382cf | ||
|
|
1b5330323c | ||
|
|
8aba4888e1 | ||
|
|
1c3b9a3859 | ||
|
|
e8f04f5a3b | ||
|
|
56b814e877 | ||
|
|
628ccd39d6 | ||
|
|
59db3f9b62 | ||
|
|
416f922b56 | ||
|
|
b52a86e6cc | ||
|
|
e523b5069e | ||
|
|
e8ae8d9807 | ||
|
|
64e56a861d | ||
|
|
c1bcd09c9b | ||
|
|
574258804f | ||
|
|
21ea3d8a2b | ||
|
|
6de4288a85 | ||
|
|
a107b5e652 | ||
|
|
b02db2c182 | ||
|
|
f8a04cda7a | ||
|
|
226e8edefc | ||
|
|
9b90b30a1f | ||
|
|
2ed245b25a | ||
|
|
4b769839d0 | ||
|
|
239dd1d5a1 | ||
|
|
37509cce9b | ||
|
|
227c71549b | ||
|
|
a10d8334f3 | ||
|
|
0fc61e52dd | ||
|
|
5e44c33bb6 | ||
|
|
df3655e958 | ||
|
|
fe43e13145 | ||
|
|
acd3aad8d9 | ||
|
|
811b4b4f22 | ||
|
|
7db3b9f401 | ||
|
|
a5a099cf43 | ||
|
|
ba605643bb | ||
|
|
96e98c1abb | ||
|
|
5de6f6ae2b | ||
|
|
0685722773 | ||
|
|
29df3b2859 | ||
|
|
fc6f859ced | ||
|
|
6b21a5416f | ||
|
|
74e7c8bbf1 | ||
|
|
757075b16a | ||
|
|
e8903c4d48 | ||
|
|
98262d16ee | ||
|
|
339506cf10 | ||
|
|
63bd5df09b | ||
|
|
32af2d0831 | ||
|
|
242b03d4b5 | ||
|
|
87a62666ac | ||
|
|
2dcf5486da | ||
|
|
60d3d8b3ae | ||
|
|
e9141d65fe | ||
|
|
aae591daec | ||
|
|
a5ca8a7472 | ||
|
|
36f8cc02cb | ||
|
|
55b9645651 | ||
|
|
d30ecbed5b | ||
|
|
0bbd27f04c | ||
|
|
3c53fb7403 | ||
|
|
7a81950819 | ||
|
|
74f212c702 | ||
|
|
36be943854 | ||
|
|
26a4fc75a5 | ||
|
|
a5ec5fc265 | ||
|
|
541ac44be4 | ||
|
|
117b555fcd | ||
|
|
a10cd04441 | ||
|
|
53dccbde2a | ||
|
|
56625dfe4b | ||
|
|
2eab5a3b7b | ||
|
|
f9e811862a | ||
|
|
25ccc3d0e1 | ||
|
|
8853bf0670 | ||
|
|
71fa3a824b | ||
|
|
56fd50834c | ||
|
|
48ba812cf6 | ||
|
|
019dc6d45f | ||
|
|
cbcb784a79 | ||
|
|
39fa0258ad | ||
|
|
d0dd5bfa8c | ||
|
|
b948c47618 | ||
|
|
18cae8ac53 | ||
|
|
0576231dfc | ||
|
|
6ad35a83dd | ||
|
|
33d4dfc745 | ||
|
|
f3bf63a668 | ||
|
|
2dbb7395a4 | ||
|
|
7c1eb80d62 | ||
|
|
f2bf093691 | ||
|
|
2f002bfa4a | ||
|
|
4a19038d54 | ||
|
|
15fb3dd92c | ||
|
|
e0982d3961 | ||
|
|
7fb2f15f54 | ||
|
|
f93dbb4116 | ||
|
|
bc34fe3a9f | ||
|
|
bbe49491c1 | ||
|
|
d5ccb80f26 | ||
|
|
50fd15379a | ||
|
|
ed479f1155 | ||
|
|
04db9ba714 | ||
|
|
6a973e3248 | ||
|
|
96d9e4977b | ||
|
|
5a3897f22a | ||
|
|
ceef00b79a | ||
|
|
94ee24ea11 | ||
|
|
1a201f2e94 | ||
|
|
e0c6d41d4b | ||
|
|
44a0ae86d2 | ||
|
|
06822ad385 | ||
|
|
3be52280ba | ||
|
|
5142f978cf | ||
|
|
667ffd4dfd | ||
|
|
17626f1853 | ||
|
|
5c79567a2c | ||
|
|
0f5fd9af62 | ||
|
|
99643537d1 | ||
|
|
47650386e0 | ||
|
|
aa3fc34646 | ||
|
|
92f5f1ac71 | ||
|
|
eb739f73c7 | ||
|
|
20a646106b | ||
|
|
0dc14517cd | ||
|
|
04713c007b | ||
|
|
1cac7660b8 | ||
|
|
edb286f9af | ||
|
|
fb15ff99ca | ||
|
|
a455e75e37 | ||
|
|
2b452e51f9 | ||
|
|
9d2b6f8854 | ||
|
|
3be10a1b59 | ||
|
|
01950c25a8 | ||
|
|
7ad1ebd0e8 | ||
|
|
ee01a8b5a0 | ||
|
|
23c54f6813 | ||
|
|
ceef4510fb | ||
|
|
042a6b943e | ||
|
|
e114b73654 | ||
|
|
da0a505978 | ||
|
|
cb6d4a385c | ||
|
|
2806aee0fc | ||
|
|
52736f5162 | ||
|
|
6d45ad7fe8 | ||
|
|
2fc53644bc | ||
|
|
b33eaec493 | ||
|
|
63d6f3ac78 | ||
|
|
fd4caac013 | ||
|
|
ada875cdb0 | ||
|
|
49ddfa573d | ||
|
|
b58ac367ee | ||
|
|
cf62b4ed95 | ||
|
|
b05c62e141 | ||
|
|
095556106c | ||
|
|
5cdd72a45c | ||
|
|
5d84f8af43 | ||
|
|
d9483d9214 | ||
|
|
250475830f | ||
|
|
b8a0a823e0 | ||
|
|
6a54d669a3 | ||
|
|
62559c4e66 | ||
|
|
e04ecaaf7a | ||
|
|
fa4706c95f | ||
|
|
7067c0e02e | ||
|
|
cc71ce6180 | ||
|
|
f409a3583c | ||
|
|
ac7504e017 | ||
|
|
345bd3f7c1 | ||
|
|
2e2960ec69 | ||
|
|
e93b103d1e | ||
|
|
22977a4c5b | ||
|
|
7da5d5322b | ||
|
|
49e2c40ba4 | ||
|
|
8041a004c2 | ||
|
|
db62d78e04 | ||
|
|
bd79318b1e | ||
|
|
2736bd9165 | ||
|
|
b6bd48584f | ||
|
|
7b4f3c975a | ||
|
|
b165fadc55 | ||
|
|
274e0d0557 | ||
|
|
7594e51a18 | ||
|
|
bf22819b53 | ||
|
|
3dea4b15e0 | ||
|
|
5836b3292b | ||
|
|
a8fd49a234 | ||
|
|
006ad72eb2 | ||
|
|
dc254e1ee5 | ||
|
|
b0d31a4d35 | ||
|
|
24155feea4 | ||
|
|
db0a4e35c6 | ||
|
|
68ef98d346 | ||
|
|
f23bb9fac1 | ||
|
|
d20dde0b6d | ||
|
|
f76b30d109 | ||
|
|
ee1a047cde | ||
|
|
4c533fedfd | ||
|
|
f8ab7b8e13 | ||
|
|
46c5eaf0e1 | ||
|
|
e7e73aa1d2 | ||
|
|
394221f3df | ||
|
|
7d2647f830 | ||
|
|
563c90a8c4 | ||
|
|
0e0352fdd6 | ||
|
|
d6a8db97d8 | ||
|
|
05f8fa21de | ||
|
|
ab8779086b | ||
|
|
ed8305b199 | ||
|
|
1def825c7f | ||
|
|
3c9b5d3b17 | ||
|
|
3c2dd88fd3 | ||
|
|
0e21d5e166 | ||
|
|
8c221bd786 | ||
|
|
3b1fcdfb10 | ||
|
|
9bb91e1085 | ||
|
|
98bee122fe | ||
|
|
6aaa9a83a6 | ||
|
|
2eaea8ce7c | ||
|
|
f5ded03b9b | ||
|
|
f509a4b3ab | ||
|
|
3f46485382 | ||
|
|
2c5e6c0788 | ||
|
|
a7d4a04ac2 | ||
|
|
d199c1b6c4 | ||
|
|
92f24d20db | ||
|
|
0dd43df7aa | ||
|
|
1675586a29 | ||
|
|
a1e511b19a | ||
|
|
5606ed1308 | ||
|
|
a301575dd7 | ||
|
|
e89e803d54 | ||
|
|
4486a126ad | ||
|
|
d740e7a521 | ||
|
|
cb8eab443c | ||
|
|
fe3b649fe9 | ||
|
|
51c85a1b10 | ||
|
|
7223efb9f8 | ||
|
|
c3296cce3d | ||
|
|
74ea87676e | ||
|
|
5ef20aba21 | ||
|
|
54eaee3f79 | ||
|
|
4c82da1f5c | ||
|
|
080dc01c21 | ||
|
|
f09fbf4ba6 | ||
|
|
9d1510f14d | ||
|
|
62ea92335d | ||
|
|
d1e2822b92 | ||
|
|
533cb747df | ||
|
|
33a0f9c97f | ||
|
|
ef9a442718 | ||
|
|
ad10413714 | ||
|
|
c9014da186 | ||
|
|
c025f9f02b | ||
|
|
b737acedb0 | ||
|
|
f15bfe3038 | ||
|
|
8938f51292 | ||
|
|
4b92b8f714 | ||
|
|
5f13b9bca4 | ||
|
|
2f219aac6f | ||
|
|
1d9efb30e2 | ||
|
|
ed7be00881 | ||
|
|
a6003f6bff | ||
|
|
4619045375 | ||
|
|
e61b8f1b34 | ||
|
|
79b6ec839a | ||
|
|
fd12e30c53 | ||
|
|
87a9ca4318 | ||
|
|
3f64411174 | ||
|
|
57b0da1a3a | ||
|
|
7d3e74a67f | ||
|
|
d704e322df | ||
|
|
f598153818 | ||
|
|
f395a0c170 | ||
|
|
654b33d27f | ||
|
|
6c12da96c9 | ||
|
|
1a6f639b81 | ||
|
|
59a00a38c9 | ||
|
|
2beee168e3 | ||
|
|
a92bbc7e5a | ||
|
|
fbc921dd07 | ||
|
|
59c6c3d777 | ||
|
|
e7ab61c8d0 | ||
|
|
7023760782 | ||
|
|
932ce435b5 | ||
|
|
af384d88f7 | ||
|
|
792846ddad | ||
|
|
1187d9c78c | ||
|
|
37db9ab072 | ||
|
|
fb40060560 | ||
|
|
a4f4887647 | ||
|
|
f1d7f556fd | ||
|
|
1e70e1d329 | ||
|
|
e02a009635 | ||
|
|
bd6ff35603 | ||
|
|
1cb7727dc7 | ||
|
|
0c73e3d0ae | ||
|
|
6371d364e1 | ||
|
|
ded9c9140d | ||
|
|
7c8a108e28 | ||
|
|
2a18ffcdba | ||
|
|
381d0d5e81 | ||
|
|
be126acfd1 | ||
|
|
fc2f5cfe4d | ||
|
|
9878902a89 | ||
|
|
f1230d46f3 | ||
|
|
d8822392f1 | ||
|
|
1d9cf71517 | ||
|
|
2160e87fef | ||
|
|
88249ba8aa | ||
|
|
2856617fb3 | ||
|
|
d822980d5a | ||
|
|
b5ba59b413 | ||
|
|
1148e0163c | ||
|
|
cf36c7adb1 | ||
|
|
eac6998e17 | ||
|
|
17afbffdb5 | ||
|
|
072a439c2d | ||
|
|
c533600983 | ||
|
|
5847fbb6b6 | ||
|
|
1876b30c1b | ||
|
|
5c71f55993 | ||
|
|
9c0960d03d | ||
|
|
29a395f3f4 | ||
|
|
a676b8d8e6 | ||
|
|
7ab0be3b62 | ||
|
|
115853fed2 | ||
|
|
60beb81ae4 | ||
|
|
5310375d42 | ||
|
|
7ce29e3a09 | ||
|
|
42c65f4f16 | ||
|
|
bf2cc2a4d5 | ||
|
|
6d6d7121f6 | ||
|
|
2ab67380d6 | ||
|
|
899387caa1 | ||
|
|
56c8a9e6fe | ||
|
|
896288a40b | ||
|
|
fc8ce5e4b9 | ||
|
|
4affc3c4ce | ||
|
|
067be54715 | ||
|
|
0dad2dc64b | ||
|
|
867b703644 | ||
|
|
3d398ef6dd | ||
|
|
85d95b2d8e | ||
|
|
d091d9db6b | ||
|
|
e081ab5239 | ||
|
|
7b12fd1ad2 | ||
|
|
80d8c5953e | ||
|
|
194037ff41 | ||
|
|
094d642739 | ||
|
|
010e03252e | ||
|
|
98638186b5 | ||
|
|
c5293ef21f | ||
|
|
366a1c91b8 | ||
|
|
157450e674 | ||
|
|
1b8d2bc81c | ||
|
|
f1787c43e5 | ||
|
|
95ea88e932 | ||
|
|
f2b8461bb9 | ||
|
|
7838a787df | ||
|
|
cc3f2ecb07 | ||
|
|
833b7c3916 | ||
|
|
90209f2ca2 | ||
|
|
2681036c32 | ||
|
|
ee58071ff1 | ||
|
|
e222f2f6c3 | ||
|
|
0b7becb161 | ||
|
|
bf795ab7a5 | ||
|
|
59df38ae8a | ||
|
|
e04ff048b8 | ||
|
|
7d25d07c6d | ||
|
|
7b0ddfae42 | ||
|
|
43fa1a7245 | ||
|
|
057530eed0 | ||
|
|
aee34f6365 | ||
|
|
5519d92243 | ||
|
|
3d95ac1f93 | ||
|
|
5c938535be | ||
|
|
2fdecc551a | ||
|
|
10204afdb4 | ||
|
|
55c800c2a5 | ||
|
|
265b3f9963 | ||
|
|
a8bf2cd1cf | ||
|
|
4fcd8b3dfe | ||
|
|
e1a1a6344d | ||
|
|
a095c10a25 | ||
|
|
b1ea487e22 | ||
|
|
47265d0d10 | ||
|
|
6a41b41a38 | ||
|
|
2247e43a48 | ||
|
|
d3986080a3 | ||
|
|
07277e1a5b | ||
|
|
1d6b5a35bd | ||
|
|
1ff6ee14ac | ||
|
|
39d7fc748f | ||
|
|
4d3a69cf6a | ||
|
|
b806522751 | ||
|
|
256fa897a7 | ||
|
|
5c812eed6c | ||
|
|
f0b22f9119 | ||
|
|
7e1884acb5 | ||
|
|
9512521783 | ||
|
|
da7904a767 | ||
|
|
3a0c1db168 | ||
|
|
bd21692323 | ||
|
|
5ae245bdca | ||
|
|
d3052cd97d | ||
|
|
336f85a31c | ||
|
|
b01f7c796e | ||
|
|
56f438fe47 | ||
|
|
baa5199b83 | ||
|
|
23ca62b304 | ||
|
|
2c3511195c | ||
|
|
d31ef481f3 | ||
|
|
a490da5e5c | ||
|
|
72d3576257 | ||
|
|
ebd93a55a0 | ||
|
|
4d01e1afe6 | ||
|
|
d2e51e97c0 | ||
|
|
9f94465979 | ||
|
|
9ff1386751 | ||
|
|
5fca35f0b1 | ||
|
|
d23c763441 | ||
|
|
fa058c4783 | ||
|
|
e0ddd5f045 | ||
|
|
d83526ff5c | ||
|
|
b7443451a4 | ||
|
|
e90e4a22c4 | ||
|
|
3a53172145 | ||
|
|
1dfcb7bc29 | ||
|
|
897bb76858 | ||
|
|
bcc67269ab | ||
|
|
4d24c654b9 | ||
|
|
cba44b091b | ||
|
|
f2d8f8a41b | ||
|
|
4b1c7e7e3c | ||
|
|
1cba278876 | ||
|
|
766898fdf9 | ||
|
|
13e91d594b | ||
|
|
ca0a0da19f | ||
|
|
82cabce86e | ||
|
|
1d6b7e1b2e | ||
|
|
1c9458d056 | ||
|
|
4e29f2ae8b | ||
|
|
69321636b5 | ||
|
|
d190b254bd | ||
|
|
51a630995a | ||
|
|
3a74f0726c | ||
|
|
efa6c7bba0 | ||
|
|
005a96f3d3 | ||
|
|
e39e0910a1 | ||
|
|
56a6cee8f2 | ||
|
|
b8141542f8 | ||
|
|
8fc9a90207 | ||
|
|
13d707d98d | ||
|
|
aae0ff6e7a | ||
|
|
69c7b5a0d5 | ||
|
|
d1ad3115fa | ||
|
|
770af402a4 | ||
|
|
3236c0b93a | ||
|
|
51aacc3f38 | ||
|
|
397c6f46f9 | ||
|
|
d00f78f859 | ||
|
|
29fec2e0de | ||
|
|
88d28665ef | ||
|
|
de1f4da258 | ||
|
|
7985be57ab | ||
|
|
a835e7aaa2 | ||
|
|
22958cfbb1 | ||
|
|
c4dc5eb9e1 | ||
|
|
db758f386e | ||
|
|
3fb3eefa94 | ||
|
|
9340dff45d | ||
|
|
2d6c756e70 | ||
|
|
03e965d449 | ||
|
|
34f72544d8 | ||
|
|
d839ea9781 | ||
|
|
2b7f13fdbb | ||
|
|
7557a3a4ae | ||
|
|
fcecba484f | ||
|
|
fa85a0a0bd | ||
|
|
dc64bfeba2 | ||
|
|
871b73c48d | ||
|
|
5dcff91d27 | ||
|
|
0041fc1dab | ||
|
|
314242ab08 | ||
|
|
09e8ddfd74 | ||
|
|
4cea483a87 | ||
|
|
99aa616188 | ||
|
|
444c4602c1 | ||
|
|
5b6eac7140 | ||
|
|
7cdd184197 | ||
|
|
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 |
No files matched your search
@@ -33,3 +33,4 @@ build/
|
|||||||
|
|
||||||
# Compiled JNI libraries folder
|
# Compiled JNI libraries folder
|
||||||
**/jniLibs
|
**/jniLibs
|
||||||
|
app/.externalNativeBuild/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "app/src/main/jni/jnienet/enet"]
|
||||||
|
path = app/src/main/jni/jnienet/enet
|
||||||
|
url = https://github.com/cgutman/enet.git
|
||||||
@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
|
|||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
local data_start = -1
|
local data_start = -1
|
||||||
while i < buf:len do
|
while i < buf:len() do
|
||||||
-- Make sure we have a potential start sequence and type
|
-- Make sure we have a potential start sequence and type
|
||||||
if buf:len() - i < 5 then
|
if buf:len() - i < 5 then
|
||||||
-- We need more data
|
-- We need more data
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#Limelight
|
#Moonlight
|
||||||
|
|
||||||
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
[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.
|
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
||||||
|
|
||||||
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
##Features
|
||||||
|
|
||||||
@@ -16,8 +18,8 @@ whether in your own home or over the internet.
|
|||||||
|
|
||||||
##Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Limelight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
* 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
|
||||||
@@ -31,7 +33,7 @@ whether in your own home or over the internet.
|
|||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Limelight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
@@ -44,7 +46,11 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
Check out our [website](http://limelight-stream.com) for project links and information.
|
##Building
|
||||||
|
* Install Android Studio and the Android NDK
|
||||||
|
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
||||||
|
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||||
|
* Build the APK using Android Studio
|
||||||
|
|
||||||
##Authors
|
##Authors
|
||||||
|
|
||||||
@@ -53,5 +59,5 @@ Check out our [website](http://limelight-stream.com) for project links and infor
|
|||||||
* [Aaron Neyer](https://github.com/Aaronneyer)
|
* [Aaron Neyer](https://github.com/Aaronneyer)
|
||||||
* [Andrew Hennessy](https://github.com/yetanothername)
|
* [Andrew Hennessy](https://github.com/yetanothername)
|
||||||
|
|
||||||
Limelight is the work of students at [Case Western](http://case.edu) and was
|
Moonlight is the work of students at [Case Western](http://case.edu) and was
|
||||||
started as a project at [MHacks](http://mhacks.org).
|
started as a project at [MHacks](http://mhacks.org).
|
||||||
+72
-31
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||||
<component name="FacetManager">
|
<component name="FacetManager">
|
||||||
<facet type="android-gradle" name="Android-Gradle">
|
<facet type="android-gradle" name="Android-Gradle">
|
||||||
<configuration>
|
<configuration>
|
||||||
@@ -9,11 +9,12 @@
|
|||||||
<facet type="android" name="Android">
|
<facet type="android" name="Android">
|
||||||
<configuration>
|
<configuration>
|
||||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||||
|
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
<afterSyncTasks>
|
||||||
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
<task>generateNonRootDebugSources</task>
|
||||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
</afterSyncTasks>
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||||
@@ -21,92 +22,132 @@
|
|||||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</facet>
|
</facet>
|
||||||
|
<facet type="native-android-gradle" name="Native-Android-Gradle">
|
||||||
|
<configuration>
|
||||||
|
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
</component>
|
</component>
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||||
|
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/nonRoot/debug" />
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/jnienet/enet" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/jnienet" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/evdev_reader" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/nv_opus_dec" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/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/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/aidl" isTestSource="false" />
|
<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/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$/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$/src/nonRootDebug/shaders" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/res" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/resources" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/assets" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/aidl" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/java" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/rs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/shaders" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/aidl" isTestSource="false" />
|
<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/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/nonRoot/rs" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/shaders" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/res" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/resources" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/assets" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/aidl" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/java" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/rs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/shaders" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/res" type="java-test-resource" />
|
<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/resources" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/assets" 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/aidl" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/java" 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/androidTestNonRoot/rs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/shaders" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
<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/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" 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/aidl" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" 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/debug/rs" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
<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/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" 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/aidl" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" 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/main/rs" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
<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/resources" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" 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/aidl" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" 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" />
|
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
<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/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/incremental" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
|
||||||
<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/manifests" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndkBuild" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
<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/res" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="jcodec-0.1.9-patched" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||
+33
-34
@@ -4,68 +4,67 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 21
|
compileSdkVersion 25
|
||||||
buildToolsVersion "21.0.2"
|
buildToolsVersion "25.0.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 21
|
targetSdkVersion 25
|
||||||
|
|
||||||
versionName "2.9"
|
versionName "4.8.2"
|
||||||
versionCode = 38
|
versionCode = 114
|
||||||
}
|
}
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
root {
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
runProguard false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.main.jni.srcDirs = []
|
// These lines are required to avoid dexing issues with the BouncyCastle library
|
||||||
|
// bundled with limelight-common.jar
|
||||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
packagingOptions {
|
||||||
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
|
exclude 'META-INF/BCKEY.SF'
|
||||||
Properties properties = new Properties()
|
exclude 'META-INF/BCKEY.DSA'
|
||||||
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) {
|
externalNativeBuild {
|
||||||
compileTask -> compileTask.dependsOn ndkBuild
|
ndkBuild {
|
||||||
|
path "src/main/jni/Android.mk"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
|
||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
|
||||||
compile files('libs/jmdns-fixed.jar')
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
|
||||||
|
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
|
||||||
|
|
||||||
|
compile files('libs/jmdns-3.4.2.jar')
|
||||||
compile files('libs/limelight-common.jar')
|
compile files('libs/limelight-common.jar')
|
||||||
compile files('libs/tinyrtsp.jar')
|
compile files('libs/tinyrtsp.jar')
|
||||||
|
compile files('libs/jcodec-0.1.9-patched.jar')
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,41 +8,61 @@
|
|||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature
|
||||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
android:name="android.hardware.touchscreen"
|
||||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.wifi"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.gamepad"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.usb.host"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:banner="@drawable/atv_banner"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<!-- Launcher for traditional devices -->
|
<!-- Samsung multi-window support -->
|
||||||
|
<uses-library
|
||||||
|
android:name="com.sec.android.app.multiwindow"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.sec.android.support.multiwindow"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
android:name=".PcView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||||
<!-- Launcher for Android TV devices -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcViewTv"
|
android:name=".AppViewShortcutTrampoline"
|
||||||
android:logo="@drawable/atv_banner"
|
android:noHistory="true"
|
||||||
android:icon="@drawable/atv_banner"
|
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<intent-filter>
|
<meta-data
|
||||||
<action android:name="android.intent.action.MAIN" />
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
android:value="com.limelight.PcView" />
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppView"
|
android:name=".AppView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
@@ -63,18 +83,32 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".Game"
|
android:name=".Game"
|
||||||
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
android:noHistory="true"
|
||||||
|
android:theme="@style/StreamTheme">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.Connection" />
|
android:value="com.limelight.AppView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".discovery.DiscoveryService"
|
android:name=".discovery.DiscoveryService"
|
||||||
android:label="mDNS PC Auto-Discovery Service" />
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
<service
|
<service
|
||||||
android:name=".computers.ComputerManagerService"
|
android:name=".computers.ComputerManagerService"
|
||||||
android:label="Computer Management Service" />
|
android:label="Computer Management Service" />
|
||||||
|
<service
|
||||||
|
android:name=".binding.input.driver.UsbDriverService"
|
||||||
|
android:label="Usb Driver Service" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".HelpActivity"
|
||||||
|
android:configChanges="mcc|mnc|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>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,104 +1,290 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.StringReader;
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.grid.AppGridAdapter;
|
||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.R;
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
import com.limelight.ui.AdapterFragment;
|
||||||
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
public class AppView extends Activity {
|
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private ListView appList;
|
private AppGridAdapter appGridAdapter;
|
||||||
private ArrayAdapter<AppObject> appListAdapter;
|
private String uuidString;
|
||||||
private InetAddress ipAddress;
|
private ShortcutHelper shortcutHelper;
|
||||||
private String uniqueId;
|
|
||||||
private boolean remote;
|
|
||||||
|
|
||||||
private final static int RESUME_ID = 1;
|
private ComputerDetails computer;
|
||||||
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
private String lastRawApplist;
|
||||||
|
private int lastRunningAppId;
|
||||||
|
private boolean suspendGridUpdates;
|
||||||
|
private boolean inForeground;
|
||||||
|
|
||||||
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
private final static int CANCEL_ID = 3;
|
private final static int CANCEL_ID = 3;
|
||||||
|
private final static int START_WTIH_QUIT = 4;
|
||||||
|
|
||||||
public final static String ADDRESS_EXTRA = "Address";
|
|
||||||
public final static String UNIQUEID_EXTRA = "UniqueId";
|
|
||||||
public final static String NAME_EXTRA = "Name";
|
public final static String NAME_EXTRA = "Name";
|
||||||
public final static String REMOTE_EXTRA = "Remote";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
|
|
||||||
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
|
|
||||||
|
// Wait in a separate thread to avoid stalling the UI
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Wait for the binder to be ready
|
||||||
|
localBinder.waitForReady();
|
||||||
|
|
||||||
|
// Now make the binder visible
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
|
// Get the computer object
|
||||||
|
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||||
|
|
||||||
|
try {
|
||||||
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
|
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||||
|
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||||
|
computer, managerBinder.getUniqueId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the app grid with cached data (if possible).
|
||||||
|
// This must be done _before_ startComputerUpdates()
|
||||||
|
// so the initial serverinfo response can update the running
|
||||||
|
// icon.
|
||||||
|
populateAppGridWithCache();
|
||||||
|
|
||||||
|
// Start updates
|
||||||
|
startComputerUpdates();
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isFinishing() || isChangingConfigurations()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Despite my best efforts to catch all conditions that could
|
||||||
|
// cause the activity to be destroyed when we try to commit
|
||||||
|
// I haven't been able to, so we have this try-catch block.
|
||||||
|
try {
|
||||||
|
getFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void startComputerUpdates() {
|
||||||
|
// Don't start polling if we're not bound or in the foreground
|
||||||
|
if (managerBinder == null || !inForeground) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||||
|
// Do nothing if updates are suspended
|
||||||
|
if (suspendGridUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't care about other computers
|
||||||
|
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state == ComputerDetails.State.OFFLINE) {
|
||||||
|
// The PC is unreachable now
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Display a toast to the user and quit the activity
|
||||||
|
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close immediately if the PC is no longer paired
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Disable shortcuts referencing this PC for now
|
||||||
|
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||||
|
getResources().getString(R.string.scut_not_paired));
|
||||||
|
|
||||||
|
// Display a toast to the user and quit the activity
|
||||||
|
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// App list is the same or empty
|
||||||
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
|
||||||
|
// Let's check if the running app ID changed
|
||||||
|
if (details.runningGameId != lastRunningAppId) {
|
||||||
|
// Update the currently running game using the app ID
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
lastRawApplist = details.rawAppList;
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (poller == null) {
|
||||||
|
poller = managerBinder.createAppListPoller(computer);
|
||||||
|
}
|
||||||
|
poller.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopComputerUpdates() {
|
||||||
|
if (poller != null) {
|
||||||
|
poller.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appGridAdapter != null) {
|
||||||
|
appGridAdapter.cancelQueuedOperations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_app_view);
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
|
||||||
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
|
||||||
if (address == null || uniqueId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
|
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||||
TextView label = (TextView) findViewById(R.id.appListText);
|
TextView label = (TextView) findViewById(R.id.appListText);
|
||||||
setTitle(labelText);
|
setTitle(labelText);
|
||||||
label.setText(labelText);
|
label.setText(labelText);
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
|
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||||
|
shortcutHelper.reportShortcutUsed(uuidString);
|
||||||
|
|
||||||
|
// Bind to the computer manager service
|
||||||
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
|
Service.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateAppGridWithCache() {
|
||||||
try {
|
try {
|
||||||
ipAddress = InetAddress.getByAddress(address);
|
// Try to load from cache
|
||||||
} catch (UnknownHostException e) {
|
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
|
||||||
return;
|
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||||
|
updateUiWithAppList(applist);
|
||||||
|
LimeLog.info("Loaded applist from cache");
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (lastRawApplist != null) {
|
||||||
|
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
LimeLog.info("Loading applist from the network");
|
||||||
|
// We'll need to load from the network
|
||||||
|
loadAppsBlocking();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup the list view
|
private void loadAppsBlocking() {
|
||||||
appList = (ListView)findViewById(R.id.pcListView);
|
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
||||||
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
|
getResources().getString(R.string.applist_refresh_msg), true);
|
||||||
appListAdapter.setNotifyOnChange(false);
|
|
||||||
appList.setAdapter(appListAdapter);
|
|
||||||
appList.setItemsCanFocus(true);
|
|
||||||
appList.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
|
||||||
long id) {
|
|
||||||
AppObject app = appListAdapter.getItem(pos);
|
|
||||||
if (app == null || app.app == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only open the context menu if something is running, otherwise start it
|
|
||||||
if (getRunningAppId() != -1) {
|
|
||||||
openContextMenu(arg1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
doStart(app.app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerForContextMenu(appList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -107,29 +293,26 @@ public class AppView extends Activity {
|
|||||||
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
SpinnerDialog.closeDialogs(this);
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
updateAppList();
|
inForeground = true;
|
||||||
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRunningAppId() {
|
@Override
|
||||||
int runningAppId = -1;
|
protected void onPause() {
|
||||||
for (int i = 0; i < appListAdapter.getCount(); i++) {
|
super.onPause();
|
||||||
AppObject app = appListAdapter.getItem(i);
|
|
||||||
if (app.app == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.app.getIsRunning()) {
|
inForeground = false;
|
||||||
runningAppId = app.app.getAppId();
|
stopComputerUpdates();
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runningAppId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -137,20 +320,15 @@ public class AppView extends Activity {
|
|||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = appListAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
if (selectedApp == null || selectedApp.app == null) {
|
if (lastRunningAppId != 0) {
|
||||||
return;
|
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
||||||
}
|
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
int runningAppId = getRunningAppId();
|
|
||||||
if (runningAppId != -1) {
|
|
||||||
if (runningAppId == selectedApp.app.getAppId()) {
|
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
|
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
|
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||||
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
|
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,16 +340,43 @@ public class AppView extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
AppObject app = appListAdapter.getItem(info.position);
|
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId()) {
|
||||||
{
|
case START_WTIH_QUIT:
|
||||||
case RESUME_ID:
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case START_OR_RESUME_ID:
|
||||||
// Resume is the same as start for us
|
// Resume is the same as start for us
|
||||||
doStart(app.app);
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
doQuit(app.app);
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
suspendGridUpdates = true;
|
||||||
|
ServerHelper.doQuit(AppView.this,
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
|
app.app, managerBinder, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Trigger a poll immediately
|
||||||
|
suspendGridUpdates = false;
|
||||||
|
if (poller != null) {
|
||||||
|
poller.pollNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CANCEL_ID:
|
case CANCEL_ID:
|
||||||
@@ -182,117 +387,153 @@ public class AppView extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateString(NvApp app) {
|
private void updateUiWithServerinfo(final ComputerDetails details) {
|
||||||
StringBuilder str = new StringBuilder();
|
|
||||||
str.append(app.getAppName());
|
|
||||||
if (app.getIsRunning()) {
|
|
||||||
str.append(" - Running");
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addListPlaceholder() {
|
|
||||||
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateAppList() {
|
|
||||||
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
|
||||||
|
|
||||||
try {
|
|
||||||
final List<NvApp> appList = httpConn.getAppList();
|
|
||||||
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
appListAdapter.clear();
|
boolean updated = false;
|
||||||
if (appList.isEmpty()) {
|
|
||||||
addListPlaceholder();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (NvApp app : appList) {
|
|
||||||
appListAdapter.add(new AppObject(generateString(app), app));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appListAdapter.notifyDataSetChanged();
|
// Look through our current app list to tag the running app
|
||||||
}
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
});
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
// Success case
|
// There can only be one or zero apps running.
|
||||||
|
if (existingApp.isRunning &&
|
||||||
|
existingApp.app.getAppId() == details.runningGameId) {
|
||||||
|
// This app was running and still is, so we're done now
|
||||||
return;
|
return;
|
||||||
} catch (GfeHttpResponseException ignored) {
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
} catch (XmlPullParserException ignored) {
|
|
||||||
} finally {
|
|
||||||
spinner.dismiss();
|
|
||||||
}
|
}
|
||||||
|
else if (existingApp.app.getAppId() == details.runningGameId) {
|
||||||
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
|
// This app wasn't running but now is
|
||||||
|
existingApp.isRunning = true;
|
||||||
|
updated = true;
|
||||||
}
|
}
|
||||||
}.start();
|
else if (existingApp.isRunning) {
|
||||||
}
|
// This app was running but now isn't
|
||||||
|
existingApp.isRunning = false;
|
||||||
private void doStart(NvApp app) {
|
updated = true;
|
||||||
Intent intent = new Intent(this, Game.class);
|
|
||||||
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doQuit(final NvApp app) {
|
|
||||||
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
|
||||||
if (httpConn.quitApp()) {
|
|
||||||
message = "Successfully quit "+app.getAppName();
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = "Failed to quit "+app.getAppName();
|
// This app wasn't running and still isn't
|
||||||
}
|
}
|
||||||
updateAppList();
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = "Failed to resolve host";
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
|
||||||
} catch (Exception e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final String toastMessage = message;
|
if (updated) {
|
||||||
runOnUiThread(new Runnable() {
|
appGridAdapter.notifyDataSetChanged();
|
||||||
@Override
|
}
|
||||||
public void run() {
|
|
||||||
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).start();
|
|
||||||
|
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean updated = false;
|
||||||
|
|
||||||
|
// First handle app updates and additions
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
|
||||||
|
// Try to update an existing app in the list first
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
// Found the app; update its properties
|
||||||
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
|
existingApp.app.setAppName(app.getAppName());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
// This app must be new
|
||||||
|
appGridAdapter.addApp(new AppObject(app));
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next handle app removals
|
||||||
|
int i = 0;
|
||||||
|
while (i < appGridAdapter.getCount()) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// Check if this app is in the latest list
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This app was removed in the latest app list
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
appGridAdapter.removeApp(existingApp);
|
||||||
|
updated = true;
|
||||||
|
|
||||||
|
// Check this same index again because the item at i+1 is now at i after
|
||||||
|
// the removal
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on to the next item
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
appGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAdapterFragmentLayoutId() {
|
||||||
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
|
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||||
|
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveAbsListView(AbsListView listView) {
|
||||||
|
listView.setAdapter(appGridAdapter);
|
||||||
|
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
|
||||||
|
// Only open the context menu if something is running, otherwise start it
|
||||||
|
if (lastRunningAppId != 0) {
|
||||||
|
openContextMenu(arg1);
|
||||||
|
} else {
|
||||||
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(listView);
|
||||||
|
listView.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public class AppObject {
|
||||||
public String text;
|
public final NvApp app;
|
||||||
public NvApp app;
|
public boolean isRunning;
|
||||||
|
|
||||||
public AppObject(String text, NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
this.text = text;
|
if (app == null) {
|
||||||
|
throw new IllegalArgumentException("app must not be null");
|
||||||
|
}
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return text;
|
return app.getAppName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
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 com.limelight.computers.ComputerManagerListener;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class AppViewShortcutTrampoline extends Activity {
|
||||||
|
private String uuidString;
|
||||||
|
|
||||||
|
private ComputerDetails computer;
|
||||||
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
|
||||||
|
public final static String UUID_EXTRA = "UUID";
|
||||||
|
|
||||||
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
|
|
||||||
|
// Wait in a separate thread to avoid stalling the UI
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Wait for the binder to be ready
|
||||||
|
localBinder.waitForReady();
|
||||||
|
|
||||||
|
// Now make the binder visible
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
|
// Get the computer object
|
||||||
|
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||||
|
|
||||||
|
// Force CMS to repoll this machine
|
||||||
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||||
|
// Don't care about other computers
|
||||||
|
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state != ComputerDetails.State.UNKNOWN) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Stop showing the spinner
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
// Close this activity
|
||||||
|
finish();
|
||||||
|
|
||||||
|
// Create a new activity stack for this launch
|
||||||
|
ArrayList<Intent> intentStack = new ArrayList<>();
|
||||||
|
Intent i;
|
||||||
|
|
||||||
|
// Add the PC view at the back (and clear the task)
|
||||||
|
i = new Intent(AppViewShortcutTrampoline.this, PcView.class);
|
||||||
|
i.setAction(Intent.ACTION_MAIN);
|
||||||
|
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intentStack.add(i);
|
||||||
|
|
||||||
|
// Take this intent's data and create an intent to start the app view
|
||||||
|
i = new Intent(getIntent());
|
||||||
|
i.setClass(AppViewShortcutTrampoline.this, AppView.class);
|
||||||
|
intentStack.add(i);
|
||||||
|
|
||||||
|
// If a game is running, we'll make the stream the top level activity
|
||||||
|
if (details.runningGameId != 0) {
|
||||||
|
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
|
||||||
|
new NvApp("app", details.runningGameId), details, managerBinder));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now start the activities
|
||||||
|
startActivities(intentStack.toArray(new Intent[]{}));
|
||||||
|
}
|
||||||
|
else if (details.state == ComputerDetails.State.OFFLINE) {
|
||||||
|
// Computer offline - display an error dialog
|
||||||
|
Dialog.displayDialog(AppViewShortcutTrampoline.this,
|
||||||
|
getResources().getString(R.string.conn_error_title),
|
||||||
|
getResources().getString(R.string.error_pc_offline),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want any more callbacks from now on, so go ahead
|
||||||
|
// and unbind from the service
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
|
// Bind to the computer manager service
|
||||||
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
|
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
||||||
|
getResources().getString(R.string.applist_connect_msg), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
managerBinder = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,61 +4,87 @@ package com.limelight;
|
|||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.input.ControllerHandler;
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.binding.input.KeyboardTranslator;
|
import com.limelight.binding.input.KeyboardTranslator;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureManager;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
import com.limelight.binding.input.TouchContext;
|
import com.limelight.binding.input.TouchContext;
|
||||||
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.binding.input.evdev.EvdevListener;
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
import com.limelight.binding.input.evdev.EvdevWatcher;
|
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||||
import com.limelight.binding.video.ConfigurableDecoderRenderer;
|
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
||||||
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
import com.limelight.nvstream.input.KeyboardPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
import com.limelight.ui.GameGestures;
|
||||||
|
import com.limelight.ui.StreamView;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
|
import android.hardware.input.InputManager;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.SurfaceView;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnGenericMotionListener;
|
import android.view.View.OnGenericMotionListener;
|
||||||
import android.view.View.OnSystemUiVisibilityChangeListener;
|
import android.view.View.OnSystemUiVisibilityChangeListener;
|
||||||
import android.view.View.OnTouchListener;
|
import android.view.View.OnTouchListener;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||||
OnSystemUiVisibilityChangeListener
|
OnSystemUiVisibilityChangeListener, GameGestures
|
||||||
{
|
{
|
||||||
private int lastMouseX = Integer.MIN_VALUE;
|
private int lastMouseX = Integer.MIN_VALUE;
|
||||||
private int lastMouseY = Integer.MIN_VALUE;
|
private int lastMouseY = Integer.MIN_VALUE;
|
||||||
private int lastButtonState = 0;
|
private int lastButtonState = 0;
|
||||||
|
|
||||||
// Only 2 touches are supported
|
// Only 2 touches are supported
|
||||||
private TouchContext[] touchContextMap = new TouchContext[2];
|
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||||
|
private long threeFingerDownTime = 0;
|
||||||
|
|
||||||
|
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||||
|
private static final int REFERENCE_VERT_RES = 720;
|
||||||
|
|
||||||
|
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||||
|
|
||||||
private ControllerHandler controllerHandler;
|
private ControllerHandler controllerHandler;
|
||||||
|
private VirtualController virtualController;
|
||||||
private KeyboardTranslator keybTranslator;
|
private KeyboardTranslator keybTranslator;
|
||||||
|
|
||||||
private PreferenceConfiguration prefConfig;
|
private PreferenceConfiguration prefConfig;
|
||||||
private Point screenSize = new Point(0, 0);
|
|
||||||
|
|
||||||
private NvConnection conn;
|
private NvConnection conn;
|
||||||
private SpinnerDialog spinner;
|
private SpinnerDialog spinner;
|
||||||
@@ -66,26 +92,56 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
|
||||||
private EvdevWatcher evdevWatcher;
|
private InputCaptureProvider inputCaptureProvider;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
private boolean grabbedInput = true;
|
private boolean grabbedInput = true;
|
||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
|
private StreamView streamView;
|
||||||
|
|
||||||
private ConfigurableDecoderRenderer decoderRenderer;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
|
||||||
|
private EnhancedDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
private WifiManager.WifiLock wifiLock;
|
private WifiManager.WifiLock wifiLock;
|
||||||
|
|
||||||
private int drFlags = 0;
|
private int drFlags = 0;
|
||||||
|
|
||||||
|
private boolean connectedToUsbDriverService = false;
|
||||||
|
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
|
||||||
|
UsbDriverService.UsbDriverBinder binder = (UsbDriverService.UsbDriverBinder) iBinder;
|
||||||
|
binder.setListener(controllerHandler);
|
||||||
|
connectedToUsbDriverService = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName componentName) {
|
||||||
|
connectedToUsbDriverService = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static final String EXTRA_HOST = "Host";
|
public static final String EXTRA_HOST = "Host";
|
||||||
public static final String EXTRA_APP = "App";
|
public static final String EXTRA_APP_NAME = "AppName";
|
||||||
|
public static final String EXTRA_APP_ID = "AppId";
|
||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
public static final String EXTRA_PC_UUID = "UUID";
|
||||||
|
public static final String EXTRA_PC_NAME = "PcName";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
// We don't want a title bar
|
// We don't want a title bar
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
@@ -115,32 +171,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
setContentView(R.layout.activity_game);
|
setContentView(R.layout.activity_game);
|
||||||
|
|
||||||
// Start the spinner
|
// Start the spinner
|
||||||
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
|
spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
||||||
|
getResources().getString(R.string.conn_establishing_msg), true);
|
||||||
|
|
||||||
// Read the stream preferences
|
// Read the stream preferences
|
||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
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) {
|
if (prefConfig.stretchVideo) {
|
||||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
Display display = getWindowManager().getDefaultDisplay();
|
|
||||||
display.getSize(screenSize);
|
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
|
streamView = (StreamView) findViewById(R.id.surfaceView);
|
||||||
sv.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
sv.setOnTouchListener(this);
|
streamView.setOnTouchListener(this);
|
||||||
|
|
||||||
// Warn the user if they're on a metered connection
|
// Warn the user if they're on a metered connection
|
||||||
checkDataConnection();
|
checkDataConnection();
|
||||||
@@ -152,74 +196,204 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
wifiLock.acquire();
|
wifiLock.acquire();
|
||||||
|
|
||||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||||
|
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||||
|
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||||
|
|
||||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||||
decoderRenderer.initializeWithFlags(drFlags);
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
|
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
||||||
|
shortcutHelper.reportShortcutUsed(uuid);
|
||||||
|
|
||||||
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
|
MediaCodecHelper.initializeWithContext(this);
|
||||||
|
|
||||||
|
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat);
|
||||||
|
|
||||||
|
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
|
||||||
|
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
|
||||||
|
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoderRenderer.isAvcSupported()) {
|
||||||
|
if (spinner != null) {
|
||||||
|
spinner.dismiss();
|
||||||
|
spinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't find an AVC decoder, we can't proceed
|
||||||
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
|
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
.setApp(app)
|
.setApp(new NvApp(appName, appId))
|
||||||
.setBitrate(prefConfig.bitrate * 1000)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
|
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
|
.setMaxPacketSize(remote ? 1024 : 1292)
|
||||||
|
.setRemote(remote)
|
||||||
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
|
.setAudioConfiguration(prefConfig.enable51Surround ?
|
||||||
|
StreamConfiguration.AUDIO_CONFIGURATION_5_1 :
|
||||||
|
StreamConfiguration.AUDIO_CONFIGURATION_STEREO)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
||||||
keybTranslator = new KeyboardTranslator(conn);
|
keybTranslator = new KeyboardTranslator(conn);
|
||||||
controllerHandler = new ControllerHandler(conn);
|
controllerHandler = new ControllerHandler(this, conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
||||||
|
|
||||||
SurfaceHolder sh = sv.getHolder();
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||||
// Set the surface to the size of the video
|
|
||||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
// Set to the optimal mode for streaming
|
||||||
}
|
prepareDisplayForRendering();
|
||||||
|
|
||||||
// Initialize touch contexts
|
// Initialize touch contexts
|
||||||
for (int i = 0; i < touchContextMap.length; i++) {
|
for (int i = 0; i < touchContextMap.length; i++) {
|
||||||
touchContextMap[i] = new TouchContext(conn, i);
|
touchContextMap[i] = new TouchContext(conn, i,
|
||||||
|
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||||
|
streamView);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LimelightBuildProps.ROOT_BUILD) {
|
// Use sustained performance mode on N+ to ensure consistent
|
||||||
// Start watching for raw input
|
// CPU availability
|
||||||
evdevWatcher = new EvdevWatcher(this);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
evdevWatcher.start();
|
getWindow().setSustainedPerformanceMode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||||
|
|
||||||
|
if (prefConfig.onscreenController) {
|
||||||
|
// create virtual onscreen controller
|
||||||
|
virtualController = new VirtualController(conn,
|
||||||
|
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
|
||||||
|
this);
|
||||||
|
virtualController.refreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.usbDriver) {
|
||||||
|
// Start the USB driver
|
||||||
|
bindService(new Intent(this, UsbDriverService.class),
|
||||||
|
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The connection will be started when the surface gets created
|
// The connection will be started when the surface gets created
|
||||||
sh.addCallback(this);
|
streamView.getHolder().addCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
|
private void prepareDisplayForRendering() {
|
||||||
{
|
Display display = getWindowManager().getDefaultDisplay();
|
||||||
// Get the visible width of the activity
|
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
|
||||||
|
|
||||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
// On M, we can explicitly set the optimal display mode
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Display.Mode bestMode = display.getMode();
|
||||||
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
|
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
||||||
|
candidate.getRefreshRate() < 63;
|
||||||
|
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||||
|
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||||
|
candidate.getPhysicalWidth() <= 4096;
|
||||||
|
|
||||||
// Calculate the new size of the SurfaceView
|
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
||||||
lp.width = (int) visibleWidth;
|
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
||||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
|
||||||
|
|
||||||
// Apply the size change
|
// On non-4K streams, we force the resolution to never change
|
||||||
sv.setLayoutParams(lp);
|
if (prefConfig.width < 3840) {
|
||||||
|
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
||||||
|
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the refresh rate doesn't regress
|
||||||
|
if (!refreshRateOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the resolution doesn't regress
|
||||||
|
if (!resolutionOk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestMode = candidate;
|
||||||
|
}
|
||||||
|
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||||
|
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||||
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
|
}
|
||||||
|
// On L, we can at least tell the OS that we want 60 Hz
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
float bestRefreshRate = display.getRefreshRate();
|
||||||
|
for (float candidate : display.getSupportedRefreshRates()) {
|
||||||
|
if (candidate > bestRefreshRate && candidate < 63) {
|
||||||
|
LimeLog.info("Examining refresh rate: "+candidate);
|
||||||
|
bestRefreshRate = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||||
|
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the display mode change
|
||||||
|
getWindow().setAttributes(windowLayoutParams);
|
||||||
|
|
||||||
|
// From 4.4 to 5.1 we can't ask for a 4K display mode, so we'll
|
||||||
|
// need to hint the OS to provide one.
|
||||||
|
boolean aspectRatioMatch = false;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
|
||||||
|
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
||||||
|
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
||||||
|
// setFixedSize so we can handle 4K properly. The only known devices that have
|
||||||
|
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||||
|
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||||
|
// 4K devices run 6.0 at the moment.
|
||||||
|
Point screenSize = new Point(0, 0);
|
||||||
|
display.getSize(screenSize);
|
||||||
|
|
||||||
|
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||||
|
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||||
|
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||||
|
LimeLog.info("Stream has compatible aspect ratio with output display");
|
||||||
|
aspectRatioMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
||||||
|
// Set the surface to the size of the video
|
||||||
|
streamView.getHolder().setFixedSize(prefConfig.width, prefConfig.height);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set the surface to scale based on the aspect ratio of the stream
|
||||||
|
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkDataConnection()
|
private void checkDataConnection()
|
||||||
{
|
{
|
||||||
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
if (mgr.isActiveNetworkMetered()) {
|
if (mgr.isActiveNetworkMetered()) {
|
||||||
displayTransientMessage("Warning: Your active network connection is metered!");
|
displayTransientMessage(getResources().getString(R.string.conn_metered));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private Runnable hideSystemUi = new Runnable() {
|
private final Runnable hideSystemUi = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
// Use immersive mode on 4.4+ or standard low profile on previous builds
|
||||||
@@ -255,6 +429,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
SpinnerDialog.closeDialogs(this);
|
SpinnerDialog.closeDialogs(this);
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
if (controllerHandler != null) {
|
||||||
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
|
inputManager.unregisterInputDeviceListener(controllerHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
wifiLock.release();
|
||||||
|
|
||||||
|
if (connectedToUsbDriverService) {
|
||||||
|
// Unbind from the discovery service
|
||||||
|
unbindService(usbDriverServiceConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn != null) {
|
||||||
|
VideoDecoderRenderer.VideoFormat videoFormat = conn.getActiveVideoFormat();
|
||||||
|
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
stopConnection();
|
stopConnection();
|
||||||
|
|
||||||
@@ -262,40 +451,41 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||||
String message = null;
|
String message = null;
|
||||||
if (averageEndToEndLat > 0) {
|
if (averageEndToEndLat > 0) {
|
||||||
message = "Average client-side frame latency: "+averageEndToEndLat+" ms";
|
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||||
if (averageDecoderLat > 0) {
|
if (averageDecoderLat > 0) {
|
||||||
message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
|
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (averageDecoderLat > 0) {
|
else if (averageDecoderLat > 0) {
|
||||||
message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
|
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the video codec to the post-stream toast
|
||||||
|
if (message != null && videoFormat != VideoDecoderRenderer.VideoFormat.Unknown) {
|
||||||
|
if (videoFormat == VideoDecoderRenderer.VideoFormat.H265) {
|
||||||
|
message += " [H.265]";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message += " [H.264]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private final Runnable toggleGrab = new Runnable() {
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
wifiLock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Runnable toggleGrab = new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
if (evdevWatcher != null) {
|
|
||||||
if (grabbedInput) {
|
if (grabbedInput) {
|
||||||
evdevWatcher.ungrabAll();
|
inputCaptureProvider.disableCapture();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
evdevWatcher.regrabAll();
|
inputCaptureProvider.enableCapture();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grabbedInput = !grabbedInput;
|
grabbedInput = !grabbedInput;
|
||||||
@@ -383,18 +573,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
InputDevice dev = event.getDevice();
|
|
||||||
if (dev == null) {
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass-through virtual navigation keys
|
// Pass-through virtual navigation keys
|
||||||
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try the controller handler first
|
// Try the controller handler first
|
||||||
boolean handled = controllerHandler.handleButtonDown(keyCode, event);
|
boolean handled = controllerHandler.handleButtonDown(event);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
@@ -407,11 +592,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eat repeat down events
|
|
||||||
if (event.getRepeatCount() > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return super.onKeyDown(keyCode, event);
|
||||||
@@ -426,18 +606,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
InputDevice dev = event.getDevice();
|
|
||||||
if (dev == null) {
|
|
||||||
return super.onKeyUp(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass-through virtual navigation keys
|
// Pass-through virtual navigation keys
|
||||||
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return super.onKeyUp(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try the controller handler first
|
// Try the controller handler first
|
||||||
boolean handled = controllerHandler.handleButtonUp(keyCode, event);
|
boolean handled = controllerHandler.handleButtonUp(event);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = keybTranslator.translate(event.getKeyCode());
|
short translated = keybTranslator.translate(event.getKeyCode());
|
||||||
@@ -471,6 +646,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showKeyboard() {
|
||||||
|
LimeLog.info("Showing keyboard overlay");
|
||||||
|
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if the event was consumed
|
// Returns true if the event was consumed
|
||||||
private boolean handleMotionEvent(MotionEvent event) {
|
private boolean handleMotionEvent(MotionEvent event) {
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
@@ -485,52 +667,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||||
{
|
{
|
||||||
// This case is for touch-based input devices
|
|
||||||
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
|
|
||||||
event.getSource() == InputDevice.SOURCE_STYLUS)
|
|
||||||
{
|
|
||||||
int actionIndex = event.getActionIndex();
|
|
||||||
|
|
||||||
int eventX = (int)event.getX(actionIndex);
|
|
||||||
int eventY = (int)event.getY(actionIndex);
|
|
||||||
|
|
||||||
TouchContext context = getTouchContext(actionIndex);
|
|
||||||
if (context == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.getActionMasked())
|
|
||||||
{
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN:
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
context.touchDownEvent(eventX, eventY);
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_POINTER_UP:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
context.touchUpEvent(eventX, eventY);
|
|
||||||
if (actionIndex == 0 && event.getPointerCount() > 1) {
|
|
||||||
// The original secondary touch now becomes primary
|
|
||||||
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
// ACTION_MOVE is special because it always has actionIndex == 0
|
|
||||||
// We'll call the move handlers for all indexes manually
|
|
||||||
for (TouchContext aTouchContextMap : touchContextMap) {
|
|
||||||
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
|
// This case is for mice
|
||||||
else if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
|
(event.getPointerCount() >= 1 &&
|
||||||
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
||||||
{
|
{
|
||||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||||
|
|
||||||
@@ -567,16 +707,116 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get relative axis values if we can
|
||||||
|
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||||
|
// Send the deltas straight from the motion event
|
||||||
|
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
|
||||||
|
(short) inputCaptureProvider.getRelativeAxisY(event));
|
||||||
|
|
||||||
|
// We have to also update the position Android thinks the cursor is at
|
||||||
|
// in order to avoid jumping when we stop moving or click.
|
||||||
|
lastMouseX = (int)event.getX();
|
||||||
|
lastMouseY = (int)event.getY();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// First process the history
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now process the current values
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
|
}
|
||||||
|
|
||||||
lastButtonState = event.getButtonState();
|
lastButtonState = event.getButtonState();
|
||||||
}
|
}
|
||||||
|
// This case is for touch-based input devices
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Unknown source
|
if (virtualController != null &&
|
||||||
|
virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) {
|
||||||
|
// Ignore presses when the virtual controller is in configuration mode
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int actionIndex = event.getActionIndex();
|
||||||
|
|
||||||
|
int eventX = (int)event.getX(actionIndex);
|
||||||
|
int eventY = (int)event.getY(actionIndex);
|
||||||
|
|
||||||
|
// Special handling for 3 finger gesture
|
||||||
|
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
|
||||||
|
event.getPointerCount() == 3) {
|
||||||
|
// Three fingers down
|
||||||
|
threeFingerDownTime = SystemClock.uptimeMillis();
|
||||||
|
|
||||||
|
// Cancel the first and second touches to avoid
|
||||||
|
// erroneous events
|
||||||
|
for (TouchContext aTouchContext : touchContextMap) {
|
||||||
|
aTouchContext.cancelTouch();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchContext context = getTouchContext(actionIndex);
|
||||||
|
if (context == null) {
|
||||||
return false;
|
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:
|
||||||
|
if (event.getPointerCount() == 1) {
|
||||||
|
// All fingers up
|
||||||
|
if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
|
||||||
|
// This is a 3 finger tap to bring up the keyboard
|
||||||
|
showKeyboard();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.touchUpEvent(eventX, eventY);
|
||||||
|
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handled a known source
|
// Handled a known source
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -609,8 +849,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Scale the deltas if the device resolution is different
|
// Scale the deltas if the device resolution is different
|
||||||
// than the stream resolution
|
// than the stream resolution
|
||||||
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
|
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
|
||||||
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
|
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
}
|
}
|
||||||
@@ -634,7 +874,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
@Override
|
@Override
|
||||||
public void stageStarting(Stage stage) {
|
public void stageStarting(Stage stage) {
|
||||||
if (spinner != null) {
|
if (spinner != null) {
|
||||||
spinner.setMessage("Starting "+stage.getName());
|
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,11 +888,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
conn.stop();
|
conn.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the Evdev watcher to allow use of captured input devices
|
// Enable cursor visibility again
|
||||||
if (evdevWatcher != null) {
|
inputCaptureProvider.disableCapture();
|
||||||
evdevWatcher.shutdown();
|
|
||||||
evdevWatcher = null;
|
// Destroy the capture provider
|
||||||
}
|
inputCaptureProvider.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -665,7 +905,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (!displayedFailureDialog) {
|
if (!displayedFailureDialog) {
|
||||||
displayedFailureDialog = true;
|
displayedFailureDialog = true;
|
||||||
stopConnection();
|
stopConnection();
|
||||||
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
||||||
|
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +917,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
||||||
|
getResources().getString(R.string.conn_terminated_msg), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,6 +932,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
connecting = false;
|
connecting = false;
|
||||||
connected = true;
|
connected = true;
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Hide the mouse cursor now. Doing it before
|
||||||
|
// dismissing the spinner seems to be undone
|
||||||
|
// when the spinner gets displayed.
|
||||||
|
inputCaptureProvider.enableCapture();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
hideSystemUi(1000);
|
hideSystemUi(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,13 +976,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (!connected && !connecting) {
|
if (!connected && !connecting) {
|
||||||
connecting = true;
|
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,
|
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||||
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
||||||
}
|
}
|
||||||
@@ -739,6 +984,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
// HACK: Android is supposed to let you return from this function
|
||||||
|
// before throwing a fit if you access the surface again. Unfortunately,
|
||||||
|
// MediaCodec often tries to access the destroyed surface and triggers
|
||||||
|
// an IllegalStateException. To workaround this, we will invoke
|
||||||
|
// the DecoderRenderer's stop function ourselves, so it will hopefully
|
||||||
|
// happen early enough to not trigger the bug
|
||||||
|
decoderRenderer.stop();
|
||||||
|
|
||||||
stopConnection();
|
stopConnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
|
public class HelpActivity extends Activity {
|
||||||
|
|
||||||
|
private SpinnerDialog loadingDialog;
|
||||||
|
private WebView webView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
webView = new WebView(this);
|
||||||
|
setContentView(webView);
|
||||||
|
|
||||||
|
// These allow the user to zoom the page
|
||||||
|
webView.getSettings().setBuiltInZoomControls(true);
|
||||||
|
webView.getSettings().setDisplayZoomControls(false);
|
||||||
|
|
||||||
|
// This sets the view to display the whole page by default
|
||||||
|
webView.getSettings().setUseWideViewPort(true);
|
||||||
|
webView.getSettings().setLoadWithOverviewMode(true);
|
||||||
|
|
||||||
|
// This allows the links to places on the same page to work
|
||||||
|
webView.getSettings().setJavaScriptEnabled(true);
|
||||||
|
|
||||||
|
webView.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||||
|
if (loadingDialog == null) {
|
||||||
|
loadingDialog = SpinnerDialog.displayDialog(HelpActivity.this,
|
||||||
|
getResources().getString(R.string.help_loading_title),
|
||||||
|
getResources().getString(R.string.help_loading_msg), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
if (loadingDialog != null) {
|
||||||
|
loadingDialog.dismiss();
|
||||||
|
loadingDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
|
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
||||||
|
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase())) {
|
||||||
|
// Allow navigation to Moonlight docs
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
webView.loadUrl(getIntent().getData().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// Back goes back through the WebView history
|
||||||
|
// until no more history remains
|
||||||
|
if (webView.canGoBack()) {
|
||||||
|
webView.goBack();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,28 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.grid.PcGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
import com.limelight.preferences.AddComputerManually;
|
import com.limelight.preferences.AddComputerManually;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.preferences.StreamSettings;
|
import com.limelight.preferences.StreamSettings;
|
||||||
|
import com.limelight.ui.AdapterFragment;
|
||||||
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.HelpLauncher;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -34,21 +43,21 @@ import android.view.MenuItem;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ImageButton;
|
||||||
import android.widget.Button;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
public class PcView extends Activity {
|
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private Button settingsButton, addComputerButton;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private ListView pcList;
|
private PcGridAdapter pcGridAdapter;
|
||||||
private ArrayAdapter<ComputerObject> pcListAdapter;
|
private ShortcutHelper shortcutHelper;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling;
|
private boolean freezeUpdates, runningPolling, inForeground;
|
||||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
@@ -89,6 +98,8 @@ public class PcView extends Activity {
|
|||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
private final static int DELETE_ID = 5;
|
private final static int DELETE_ID = 5;
|
||||||
|
private final static int RESUME_ID = 6;
|
||||||
|
private final static int QUIT_ID = 7;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -99,35 +110,10 @@ public class PcView extends Activity {
|
|||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
settingsButton = (Button)findViewById(R.id.settingsButton);
|
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
||||||
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
||||||
|
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
|
||||||
|
|
||||||
pcList = (ListView)findViewById(R.id.pcListView);
|
|
||||||
pcList.setAdapter(pcListAdapter);
|
|
||||||
pcList.setItemsCanFocus(true);
|
|
||||||
pcList.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
|
||||||
long id) {
|
|
||||||
ComputerObject computer = pcListAdapter.getItem(pos);
|
|
||||||
if (computer.details == null) {
|
|
||||||
// Placeholder item; no context menu for it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
|
||||||
// Open the context menu if a PC is offline
|
|
||||||
openContextMenu(arg1);
|
|
||||||
}
|
|
||||||
else if (computer.details.pairState != PairState.PAIRED) {
|
|
||||||
// Pair an unpaired machine by default
|
|
||||||
doPair(computer.details);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
doAppList(computer.details);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerForContextMenu(pcList);
|
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
@@ -141,35 +127,55 @@ public class PcView extends Activity {
|
|||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
helpButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
HelpLauncher.launchSetupGuide(PcView.this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (pcListAdapter.isEmpty()) {
|
getFragmentManager().beginTransaction()
|
||||||
addListPlaceholder();
|
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
|
||||||
|
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
||||||
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pcListAdapter.notifyDataSetChanged();
|
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
|
pcGridAdapter = new PcGridAdapter(this,
|
||||||
pcListAdapter.setNotifyOnChange(false);
|
PreferenceConfiguration.readPreferences(this).listMode,
|
||||||
|
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
||||||
|
|
||||||
initializeViews();
|
initializeViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder != null) {
|
// Only allow polling to start if we're bound to CMS, polling is not already running,
|
||||||
if (runningPolling) {
|
// and our activity is in the foreground.
|
||||||
return;
|
if (managerBinder != null && !runningPolling && inForeground) {
|
||||||
}
|
|
||||||
|
|
||||||
freezeUpdates = false;
|
freezeUpdates = false;
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -178,7 +184,7 @@ public class PcView extends Activity {
|
|||||||
PcView.this.runOnUiThread(new Runnable() {
|
PcView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
updateListView(details);
|
updateComputer(details);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -219,6 +225,7 @@ public class PcView extends Activity {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +233,7 @@ public class PcView extends Activity {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
|
inForeground = false;
|
||||||
stopComputerUpdates(false);
|
stopComputerUpdates(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,90 +252,111 @@ public class PcView extends Activity {
|
|||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = pcListAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
if (computer == null || computer.details == null) {
|
|
||||||
startComputerUpdates();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
||||||
menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
|
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
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) {
|
else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
|
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
|
if (computer.details.runningGameId != 0) {
|
||||||
menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
|
|
||||||
|
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
||||||
|
// it with delete which actually work
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
||||||
|
// startComputerUpdates() manages this and won't actual start polling until the activity
|
||||||
|
// returns to the foreground.
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (computer.runningGameId != 0) {
|
if (computer.runningGameId != 0) {
|
||||||
Toast.makeText(PcView.this, "Computer is currently in a game. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
|
||||||
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
InetAddress addr = null;
|
InetAddress addr;
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
addr = computer.localIp;
|
addr = computer.localIp;
|
||||||
}
|
}
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
addr = computer.remoteIp;
|
addr = computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
PlatformBinding.getDeviceName(),
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
message = "Already paired";
|
// Don't display any toast, but open the app list
|
||||||
|
message = null;
|
||||||
|
success = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
final String pinStr = PairingManager.generatePinString();
|
final String pinStr = PairingManager.generatePinString();
|
||||||
|
|
||||||
// Spin the dialog off in a thread because it blocks
|
// Spin the dialog off in a thread because it blocks
|
||||||
Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
|
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);
|
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||||
message = "Incorrect PIN";
|
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.FAILED) {
|
else if (pairState == PairingManager.PairState.FAILED) {
|
||||||
message = "Pairing failed";
|
message = getResources().getString(R.string.pair_fail);
|
||||||
|
}
|
||||||
|
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
||||||
|
message = getResources().getString(R.string.pair_already_in_progress);
|
||||||
}
|
}
|
||||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||||
message = "Paired successfully";
|
// Just navigate to the app view without displaying a toast
|
||||||
|
message = null;
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
// Invalidate reachability information after pairing to force
|
||||||
|
// a refresh before reading pair state again
|
||||||
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should be no other values
|
// Should be no other values
|
||||||
@@ -335,52 +364,60 @@ public class PcView extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
message = "Failed to resolve host";
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
message = getResources().getString(R.string.error_404);
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
|
final boolean toastSuccess = success;
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
if (toastMessage != null) {
|
||||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Start polling again
|
if (toastSuccess) {
|
||||||
|
// Open the app list after a successful pairing attempt
|
||||||
|
doAppList(computer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Start polling again if we're still in the foreground
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doWakeOnLan(final ComputerDetails computer) {
|
private void doWakeOnLan(final ComputerDetails computer) {
|
||||||
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
|
if (computer.state == ComputerDetails.State.ONLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer.macAddress == null) {
|
if (computer.macAddress == null) {
|
||||||
Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
WakeOnLanSender.sendWolPacket(computer);
|
WakeOnLanSender.sendWolPacket(computer);
|
||||||
message = "It may take a few seconds for your PC to wake up. " +
|
message = getResources().getString(R.string.wol_waking_msg);
|
||||||
"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
message = "Failed to send Wake-On-LAN packets";
|
message = getResources().getString(R.string.wol_fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
@@ -396,29 +433,32 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void doUnpair(final ComputerDetails computer) {
|
private void doUnpair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show();
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
InetAddress addr = null;
|
InetAddress addr;
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
addr = computer.localIp;
|
addr = computer.localIp;
|
||||||
}
|
}
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
addr = computer.remoteIp;
|
addr = computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
@@ -427,20 +467,19 @@ public class PcView extends Activity {
|
|||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
httpConn.unpair();
|
httpConn.unpair();
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
|
||||||
message = "Unpaired successfully";
|
message = getResources().getString(R.string.unpair_success);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = "Failed to unpair";
|
message = getResources().getString(R.string.unpair_fail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
message = "Device was not paired";
|
message = getResources().getString(R.string.unpair_error);
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
message = "Failed to resolve host";
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
message = getResources().getString(R.string.error_404);
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
@@ -458,36 +497,25 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
private void doAppList(ComputerDetails computer) {
|
private void doAppList(ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent i = new Intent(this, AppView.class);
|
Intent i = new Intent(this, AppView.class);
|
||||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
|
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
|
||||||
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
|
|
||||||
i.putExtra(AppView.REMOTE_EXTRA, false);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
|
|
||||||
i.putExtra(AppView.REMOTE_EXTRA, true);
|
|
||||||
}
|
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
ComputerObject computer = pcListAdapter.getItem(info.position);
|
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId()) {
|
||||||
{
|
|
||||||
case PAIR_ID:
|
case PAIR_ID:
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
return true;
|
return true;
|
||||||
@@ -502,126 +530,147 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
case DELETE_ID:
|
case DELETE_ID:
|
||||||
if (managerBinder == null) {
|
if (managerBinder == null) {
|
||||||
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
managerBinder.removeComputer(computer.details.name);
|
managerBinder.removeComputer(computer.details.name);
|
||||||
removeListView(computer.details);
|
removeComputer(computer.details);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case APP_LIST_ID:
|
||||||
doAppList(computer.details);
|
doAppList(computer.details);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case RESUME_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case QUIT_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ServerHelper.doQuit(PcView.this,
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||||
|
new NvApp("app", 0), managerBinder, null);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateString(ComputerDetails details) {
|
private void removeComputer(ComputerDetails details) {
|
||||||
StringBuilder str = new StringBuilder();
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
str.append(details.name).append(" - ");
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
|
||||||
str.append("Online ");
|
|
||||||
if (details.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
str.append("(Local) - ");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("(Remote) - ");
|
|
||||||
}
|
|
||||||
if (details.pairState == PairState.PAIRED) {
|
|
||||||
if (details.runningGameId == 0) {
|
|
||||||
str.append("Available");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("In Game");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("Not Paired");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
str.append("Offline");
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addListPlaceholder() {
|
|
||||||
pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
|
|
||||||
"If your PC doesn't show up in about 15 seconds, " +
|
|
||||||
"make sure your computer is running GFE or add your PC manually using the button above.", null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeListView(ComputerDetails details) {
|
|
||||||
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
|
||||||
ComputerObject computer = pcListAdapter.getItem(i);
|
|
||||||
|
|
||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
pcListAdapter.remove(computer);
|
// Disable or delete shortcuts referencing this PC
|
||||||
|
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||||
|
getResources().getString(R.string.scut_deleted_pc));
|
||||||
|
|
||||||
|
pcGridAdapter.removeComputer(computer);
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
|
// Show the "Discovery in progress" view
|
||||||
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pcListAdapter.getCount() == 0) {
|
|
||||||
// Add the placeholder if we're down to 0 computers
|
|
||||||
addListPlaceholder();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateListView(ComputerDetails details) {
|
private void updateComputer(ComputerDetails details) {
|
||||||
String computerString = generateString(details);
|
|
||||||
ComputerObject existingEntry = null;
|
ComputerObject existingEntry = null;
|
||||||
boolean placeholderPresent = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < pcListAdapter.getCount(); i++) {
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
ComputerObject computer = pcListAdapter.getItem(i);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
// If there's a placeholder, there's nothing else
|
|
||||||
if (computer.details == null) {
|
|
||||||
placeholderPresent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (details.equals(computer.details)) {
|
if (details.uuid.equals(computer.details.uuid)) {
|
||||||
existingEntry = computer;
|
existingEntry = computer;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC
|
||||||
|
if (details.pairState == PairState.PAIRED) {
|
||||||
|
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
// Replace the information in the existing entry
|
// Replace the information in the existing entry
|
||||||
existingEntry.text = computerString;
|
|
||||||
existingEntry.details = details;
|
existingEntry.details = details;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If the placeholder is the only object, remove it
|
|
||||||
if (placeholderPresent) {
|
|
||||||
pcListAdapter.remove(pcListAdapter.getItem(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new entry
|
// Add a new entry
|
||||||
pcListAdapter.add(new ComputerObject(computerString, details));
|
pcGridAdapter.addComputer(new ComputerObject(details));
|
||||||
|
|
||||||
|
// Remove the "Discovery in progress" view
|
||||||
|
noPcFoundLayout.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the view that the data has changed
|
// Notify the view that the data has changed
|
||||||
pcListAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAdapterFragmentLayoutId() {
|
||||||
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
|
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
|
||||||
|
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveAbsListView(AbsListView listView) {
|
||||||
|
listView.setAdapter(pcGridAdapter);
|
||||||
|
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||||
|
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||||
|
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
// Open the context menu if a PC is offline or refreshing
|
||||||
|
openContextMenu(arg1);
|
||||||
|
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
|
// Pair an unpaired machine by default
|
||||||
|
doPair(computer.details);
|
||||||
|
} else {
|
||||||
|
doAppList(computer.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerForContextMenu(listView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ComputerObject {
|
public class ComputerObject {
|
||||||
public String text;
|
|
||||||
public ComputerDetails details;
|
public ComputerDetails details;
|
||||||
|
|
||||||
public ComputerObject(String text, ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
this.text = text;
|
if (details == null) {
|
||||||
|
throw new IllegalArgumentException("details must not be null");
|
||||||
|
}
|
||||||
this.details = details;
|
this.details = details;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return text;
|
return details.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
/* This is a dummy class to allow for a separate icon
|
|
||||||
* and launcher for TV.
|
|
||||||
*/
|
|
||||||
public class PcViewTv extends PcView {}
|
|
||||||
@@ -9,14 +9,13 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
|||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
public static final int FRAME_SIZE = 960;
|
|
||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean streamInitialized(int channelCount, int sampleRate) {
|
public boolean streamInitialized(int channelCount, int channelMask, int samplesPerFrame, int sampleRate) {
|
||||||
int channelConfig;
|
int channelConfig;
|
||||||
int bufferSize;
|
int bufferSize;
|
||||||
|
int bytesPerFrame = (samplesPerFrame * 2);
|
||||||
|
|
||||||
switch (channelCount)
|
switch (channelCount)
|
||||||
{
|
{
|
||||||
@@ -26,28 +25,61 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
case 2:
|
case 2:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
break;
|
break;
|
||||||
|
case 4:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
// We're not supposed to request less than the minimum
|
||||||
channelConfig,
|
// buffer size for our buffer, but it appears that we can
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
// do this on many devices and it lowers audio latency.
|
||||||
FRAME_SIZE * 2);
|
// 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 = bytesPerFrame * 2;
|
||||||
|
|
||||||
// Round to next frame
|
|
||||||
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
|
||||||
|
|
||||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
bufferSize,
|
bufferSize,
|
||||||
AudioTrack.MODE_STREAM);
|
AudioTrack.MODE_STREAM);
|
||||||
|
|
||||||
track.play();
|
track.play();
|
||||||
|
} 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),
|
||||||
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
|
// Round to next frame
|
||||||
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
|
||||||
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
|
|||||||
|
|
||||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||||
|
|
||||||
private File certFile;
|
private final File certFile;
|
||||||
private File keyFile;
|
private final File keyFile;
|
||||||
|
|
||||||
private X509Certificate cert;
|
private X509Certificate cert;
|
||||||
private RSAPrivateKey key;
|
private RSAPrivateKey key;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
/**
|
/**
|
||||||
* GFE's prefix for every key code
|
* GFE's prefix for every key code
|
||||||
*/
|
*/
|
||||||
public static final short KEY_PREFIX = (short) 0x80;
|
private static final short KEY_PREFIX = (short) 0x80;
|
||||||
|
|
||||||
public static final int VK_0 = 48;
|
public static final int VK_0 = 48;
|
||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
|
|||||||
@@ -1,25 +1,45 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class TouchContext {
|
public class TouchContext {
|
||||||
private int lastTouchX = 0;
|
private int lastTouchX = 0;
|
||||||
private int lastTouchY = 0;
|
private int lastTouchY = 0;
|
||||||
private int originalTouchX = 0;
|
private int originalTouchX = 0;
|
||||||
private int originalTouchY = 0;
|
private int originalTouchY = 0;
|
||||||
private long originalTouchTime = 0;
|
private long originalTouchTime = 0;
|
||||||
|
private boolean cancelled;
|
||||||
|
private boolean confirmedMove;
|
||||||
|
private boolean confirmedDrag;
|
||||||
|
private Timer dragTimer;
|
||||||
|
private double distanceMoved;
|
||||||
|
private double xFactor, yFactor;
|
||||||
|
|
||||||
private NvConnection conn;
|
private final NvConnection conn;
|
||||||
private int actionIndex;
|
private final int actionIndex;
|
||||||
|
private final int referenceWidth;
|
||||||
|
private final int referenceHeight;
|
||||||
|
private final View targetView;
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||||
|
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
|
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex)
|
public TouchContext(NvConnection conn, int actionIndex,
|
||||||
|
int referenceWidth, int referenceHeight, View view)
|
||||||
{
|
{
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.actionIndex = actionIndex;
|
this.actionIndex = actionIndex;
|
||||||
|
this.referenceWidth = referenceWidth;
|
||||||
|
this.referenceHeight = referenceHeight;
|
||||||
|
this.targetView = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getActionIndex()
|
public int getActionIndex()
|
||||||
@@ -27,15 +47,19 @@ public class TouchContext {
|
|||||||
return actionIndex;
|
return actionIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isWithinTapBounds(int touchX, int touchY)
|
||||||
|
{
|
||||||
|
int xDelta = Math.abs(touchX - originalTouchX);
|
||||||
|
int yDelta = Math.abs(touchY - originalTouchY);
|
||||||
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
yDelta <= TAP_MOVEMENT_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTap()
|
private boolean isTap()
|
||||||
{
|
{
|
||||||
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
|
||||||
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||||
|
|
||||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
|
||||||
timeDelta <= TAP_TIME_THRESHOLD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte getMouseButtonIndex()
|
private byte getMouseButtonIndex()
|
||||||
@@ -50,19 +74,41 @@ public class TouchContext {
|
|||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
|
// Get the view dimensions to scale inputs on this touch
|
||||||
|
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||||
|
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
originalTouchX = lastTouchX = eventX;
|
||||||
originalTouchY = lastTouchY = eventY;
|
originalTouchY = lastTouchY = eventY;
|
||||||
originalTouchTime = System.currentTimeMillis();
|
originalTouchTime = System.currentTimeMillis();
|
||||||
|
cancelled = confirmedDrag = confirmedMove = false;
|
||||||
|
distanceMoved = 0;
|
||||||
|
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
// Start the timer for engaging a drag
|
||||||
|
startDragTimer();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (isTap())
|
if (cancelled) {
|
||||||
{
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the drag timer
|
||||||
|
cancelDragTimer();
|
||||||
|
|
||||||
byte buttonIndex = getMouseButtonIndex();
|
byte buttonIndex = getMouseButtonIndex();
|
||||||
|
|
||||||
|
if (confirmedDrag) {
|
||||||
|
// Raise the button after a drag
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
else if (isTap())
|
||||||
|
{
|
||||||
// Lower the mouse button
|
// Lower the mouse button
|
||||||
conn.sendMouseButtonDown(buttonIndex);
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
|
||||||
@@ -77,20 +123,119 @@ public class TouchContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized void startDragTimer() {
|
||||||
|
dragTimer = new Timer(true);
|
||||||
|
dragTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (TouchContext.this) {
|
||||||
|
// Check if someone already set move
|
||||||
|
if (confirmedMove) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (dragTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
dragTimer = null;
|
||||||
|
|
||||||
|
// We haven't been cancelled before the timer expired so begin dragging
|
||||||
|
confirmedDrag = true;
|
||||||
|
conn.sendMouseButtonDown(getMouseButtonIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, DRAG_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelDragTimer() {
|
||||||
|
if (dragTimer != null) {
|
||||||
|
dragTimer.cancel();
|
||||||
|
dragTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
|
||||||
|
// If we've already confirmed something, get out now
|
||||||
|
if (confirmedMove || confirmedDrag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it leaves the tap bounds before the drag time expires, it's a move.
|
||||||
|
if (!isWithinTapBounds(eventX, eventY)) {
|
||||||
|
confirmedMove = true;
|
||||||
|
cancelDragTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the maximum distance moved
|
||||||
|
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
|
||||||
|
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
|
||||||
|
confirmedMove = true;
|
||||||
|
cancelDragTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
{
|
{
|
||||||
// We only send moves for the primary touch point
|
// We only send moves and drags for the primary touch point
|
||||||
if (actionIndex == 0) {
|
if (actionIndex == 0) {
|
||||||
conn.sendMouseMove((short)(eventX - lastTouchX),
|
checkForConfirmedMove(eventX, eventY);
|
||||||
(short)(eventY - lastTouchY));
|
|
||||||
|
int deltaX = eventX - lastTouchX;
|
||||||
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
|
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
||||||
|
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
||||||
|
|
||||||
|
// Fix up the signs
|
||||||
|
if (eventX < lastTouchX) {
|
||||||
|
deltaX = -deltaX;
|
||||||
|
}
|
||||||
|
if (eventY < lastTouchY) {
|
||||||
|
deltaY = -deltaY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
||||||
|
// non-zero to update lastTouch that way devices that report small touch events often
|
||||||
|
// will work correctly
|
||||||
|
if (deltaX != 0) {
|
||||||
|
lastTouchX = eventX;
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
lastTouchY = eventY;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
|
}
|
||||||
|
else {
|
||||||
lastTouchX = eventX;
|
lastTouchX = eventX;
|
||||||
lastTouchY = eventY;
|
lastTouchY = eventY;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void cancelTouch() {
|
||||||
|
cancelled = true;
|
||||||
|
|
||||||
|
// Cancel the drag timer
|
||||||
|
cancelDragTimer();
|
||||||
|
|
||||||
|
// If it was a confirmed drag, we'll need to raise the button now
|
||||||
|
if (confirmedDrag) {
|
||||||
|
conn.sendMouseButtonUp(getMouseButtonIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.PointerIcon;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
public class AndroidCaptureProvider extends InputCaptureProvider {
|
||||||
|
private ViewGroup rootViewGroup;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public AndroidCaptureProvider(Activity activity) {
|
||||||
|
this.context = activity;
|
||||||
|
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPointerIconOnAllViews(PointerIcon icon) {
|
||||||
|
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
||||||
|
View view = rootViewGroup.getChildAt(i);
|
||||||
|
view.setPointerIcon(icon);
|
||||||
|
}
|
||||||
|
rootViewGroup.setPointerIcon(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
setPointerIconOnAllViews(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
||||||
|
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
|
public class InputCaptureManager {
|
||||||
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
|
// Shield capture is preferred because it can capture when the cursor is over
|
||||||
|
// the system UI. Android N native capture can only capture over views owned
|
||||||
|
// by the application.
|
||||||
|
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
|
return new ShieldCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
|
return new EvdevCaptureProvider(activity, rootListener);
|
||||||
|
}
|
||||||
|
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Android N+ native mouse capture");
|
||||||
|
return new AndroidCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Mouse capture not available");
|
||||||
|
return new NullCaptureProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
public abstract class InputCaptureProvider {
|
||||||
|
public abstract void enableCapture();
|
||||||
|
public abstract void disableCapture();
|
||||||
|
public void destroy() {}
|
||||||
|
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
|
public class NullCaptureProvider extends InputCaptureProvider {
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.input.InputManager;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
||||||
|
// mode without having to grab the input device (which requires root). The data comes in the form
|
||||||
|
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
||||||
|
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
||||||
|
//
|
||||||
|
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
||||||
|
|
||||||
|
public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||||
|
private static boolean nvExtensionSupported;
|
||||||
|
private static Method methodSetCursorVisibility;
|
||||||
|
private static int AXIS_RELATIVE_X;
|
||||||
|
private static int AXIS_RELATIVE_Y;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
||||||
|
|
||||||
|
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
||||||
|
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
||||||
|
|
||||||
|
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
||||||
|
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
||||||
|
|
||||||
|
nvExtensionSupported = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
nvExtensionSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShieldCaptureProvider(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return nvExtensionSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setCursorVisibility(boolean visible) {
|
||||||
|
try {
|
||||||
|
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
setCursorVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
setCursorVisibility(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||||
|
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public abstract class AbstractController {
|
||||||
|
|
||||||
|
private final int deviceId;
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
|
||||||
|
protected short buttonFlags;
|
||||||
|
protected float leftTrigger, rightTrigger;
|
||||||
|
protected float rightStickX, rightStickY;
|
||||||
|
protected float leftStickX, leftStickY;
|
||||||
|
|
||||||
|
public int getControllerId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setButtonFlag(int buttonFlag, int data) {
|
||||||
|
if (data != 0) {
|
||||||
|
buttonFlags |= buttonFlag;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonFlags &= ~buttonFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void reportInput() {
|
||||||
|
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean start();
|
||||||
|
public abstract void stop();
|
||||||
|
|
||||||
|
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceRemoved() {
|
||||||
|
listener.deviceRemoved(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceAdded() {
|
||||||
|
listener.deviceAdded(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
import android.hardware.usb.UsbEndpoint;
|
||||||
|
import android.hardware.usb.UsbInterface;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public abstract class AbstractXboxController extends AbstractController {
|
||||||
|
protected final UsbDevice device;
|
||||||
|
protected final UsbDeviceConnection connection;
|
||||||
|
|
||||||
|
private Thread inputThread;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
protected UsbEndpoint inEndpt, outEndpt;
|
||||||
|
|
||||||
|
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(deviceId, listener);
|
||||||
|
this.device = device;
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread createInputThread() {
|
||||||
|
return new Thread() {
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted() && !stopped) {
|
||||||
|
byte[] buffer = new byte[64];
|
||||||
|
|
||||||
|
int res;
|
||||||
|
|
||||||
|
//
|
||||||
|
// There's no way that I can tell to determine if a device has failed
|
||||||
|
// or if the timeout has simply expired. We'll check how long the transfer
|
||||||
|
// took to fail and assume the device failed if it happened before the timeout
|
||||||
|
// expired.
|
||||||
|
//
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Read the next input state packet
|
||||||
|
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||||
|
|
||||||
|
// If we get a zero length response, treat it as an error
|
||||||
|
if (res == 0) {
|
||||||
|
res = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||||
|
LimeLog.warning("Detected device I/O error");
|
||||||
|
AbstractXboxController.this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (res == -1 && !isInterrupted() && !stopped);
|
||||||
|
|
||||||
|
if (res == -1 || stopped) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
||||||
|
// Report input if handleRead() returns true
|
||||||
|
reportInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean start() {
|
||||||
|
// Force claim all interfaces
|
||||||
|
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||||
|
UsbInterface iface = device.getInterface(i);
|
||||||
|
|
||||||
|
if (!connection.claimInterface(iface, true)) {
|
||||||
|
LimeLog.warning("Failed to claim interfaces");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
UsbInterface iface = device.getInterface(0);
|
||||||
|
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||||
|
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||||
|
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||||
|
if (inEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate IN endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inEndpt = endpt;
|
||||||
|
}
|
||||||
|
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||||
|
if (outEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate OUT endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outEndpt = endpt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (inEndpt == null || outEndpt == null) {
|
||||||
|
LimeLog.warning("Missing required endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the init function
|
||||||
|
if (!doInit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report that we're added _before_ starting the input thread
|
||||||
|
notifyDeviceAdded();
|
||||||
|
|
||||||
|
// Start listening for controller input
|
||||||
|
inputThread = createInputThread();
|
||||||
|
inputThread.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped = true;
|
||||||
|
|
||||||
|
// Stop the input thread
|
||||||
|
if (inputThread != null) {
|
||||||
|
inputThread.interrupt();
|
||||||
|
inputThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the USB connection
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
// Report the device removed
|
||||||
|
notifyDeviceRemoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||||
|
protected abstract boolean doInit();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public interface UsbDriverListener {
|
||||||
|
void reportControllerState(int controllerId, short buttonFlags,
|
||||||
|
float leftStickX, float leftStickY,
|
||||||
|
float rightStickX, float rightStickY,
|
||||||
|
float leftTrigger, float rightTrigger);
|
||||||
|
|
||||||
|
void deviceRemoved(int controllerId);
|
||||||
|
void deviceAdded(int controllerId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class UsbDriverService extends Service implements UsbDriverListener {
|
||||||
|
|
||||||
|
private static final String ACTION_USB_PERMISSION =
|
||||||
|
"com.limelight.USB_PERMISSION";
|
||||||
|
|
||||||
|
private UsbManager usbManager;
|
||||||
|
|
||||||
|
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||||
|
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||||
|
|
||||||
|
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
private int nextDeviceId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
||||||
|
// Call through to the client's listener
|
||||||
|
if (listener != null) {
|
||||||
|
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deviceRemoved(int controllerId) {
|
||||||
|
// Remove the the controller from our list (if not removed already)
|
||||||
|
for (AbstractController controller : controllers) {
|
||||||
|
if (controller.getControllerId() == controllerId) {
|
||||||
|
controllers.remove(controller);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call through to the client's listener
|
||||||
|
if (listener != null) {
|
||||||
|
listener.deviceRemoved(controllerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deviceAdded(int controllerId) {
|
||||||
|
// Call through to the client's listener
|
||||||
|
if (listener != null) {
|
||||||
|
listener.deviceAdded(controllerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsbEventReceiver extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
|
||||||
|
// Initial attachment broadcast
|
||||||
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
|
// Continue the state machine
|
||||||
|
handleUsbDeviceState(device);
|
||||||
|
}
|
||||||
|
// Subsequent permission dialog completion intent
|
||||||
|
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||||
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
|
// If we got this far, we've already found we're able to handle this device
|
||||||
|
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||||
|
handleUsbDeviceState(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsbDriverBinder extends Binder {
|
||||||
|
public void setListener(UsbDriverListener listener) {
|
||||||
|
UsbDriverService.this.listener = listener;
|
||||||
|
|
||||||
|
// Report all controllerMap that already exist
|
||||||
|
if (listener != null) {
|
||||||
|
for (AbstractController controller : controllers) {
|
||||||
|
listener.deviceAdded(controller.getControllerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDeviceState(UsbDevice device) {
|
||||||
|
// Are we able to operate it?
|
||||||
|
if (shouldClaimDevice(device)) {
|
||||||
|
// Do we have permission yet?
|
||||||
|
if (!usbManager.hasPermission(device)) {
|
||||||
|
// Let's ask for permission
|
||||||
|
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the device
|
||||||
|
UsbDeviceConnection connection = usbManager.openDevice(device);
|
||||||
|
if (connection == null) {
|
||||||
|
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AbstractController controller;
|
||||||
|
|
||||||
|
if (XboxOneController.canClaimDevice(device)) {
|
||||||
|
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||||
|
}
|
||||||
|
else if (Xbox360Controller.canClaimDevice(device)) {
|
||||||
|
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Unreachable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the controller
|
||||||
|
if (!controller.start()) {
|
||||||
|
connection.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this controller to the list
|
||||||
|
controllers.add(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRecognizedInputDevice(UsbDevice device) {
|
||||||
|
// On KitKat and later, we can determine if this VID and PID combo
|
||||||
|
// matches an existing input device and defer to the built-in controller
|
||||||
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
|
InputDevice inputDev = InputDevice.getDevice(id);
|
||||||
|
|
||||||
|
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||||
|
inputDev.getProductId() == device.getProductId()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldClaimDevice(UsbDevice device) {
|
||||||
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
|
// if we know the kernel isn't already driving this device.
|
||||||
|
return XboxOneController.canClaimDevice(device) ||
|
||||||
|
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
|
|
||||||
|
// Register for USB attach broadcasts and permission completions
|
||||||
|
IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||||
|
filter.addAction(ACTION_USB_PERMISSION);
|
||||||
|
registerReceiver(receiver, filter);
|
||||||
|
|
||||||
|
// Enumerate existing devices
|
||||||
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
|
if (shouldClaimDevice(dev)) {
|
||||||
|
// Start the process of claiming this device
|
||||||
|
handleUsbDeviceState(dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
// Stop the attachment receiver
|
||||||
|
unregisterReceiver(receiver);
|
||||||
|
|
||||||
|
// Remove listeners
|
||||||
|
listener = null;
|
||||||
|
|
||||||
|
// Stop all controllers
|
||||||
|
while (controllers.size() > 0) {
|
||||||
|
// Stop and remove the controller
|
||||||
|
controllers.remove(0).stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class Xbox360Controller extends AbstractXboxController {
|
||||||
|
private static final int XB360_IFACE_SUBCLASS = 93;
|
||||||
|
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x12ab, // Unknown
|
||||||
|
0x1430, // RedOctane
|
||||||
|
0x146b, // BigBen
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x24c6, // PowerA
|
||||||
|
0x1532, // Razer Sabertooth
|
||||||
|
0x15e4, // Numark
|
||||||
|
0x162e, // Joytech
|
||||||
|
};
|
||||||
|
|
||||||
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (device.getVendorId() == supportedVid &&
|
||||||
|
device.getInterfaceCount() >= 1 &&
|
||||||
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||||
|
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(device, connection, deviceId, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int unsignByte(byte b) {
|
||||||
|
if (b < 0) {
|
||||||
|
return b + 256;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
|
if (buffer.limit() < 14) {
|
||||||
|
LimeLog.severe("Read too small: "+buffer.limit());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip first short
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
|
||||||
|
// DPAD
|
||||||
|
byte b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
||||||
|
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
||||||
|
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
// Start/Select
|
||||||
|
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
||||||
|
|
||||||
|
// LS/RS
|
||||||
|
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
// ABXY buttons
|
||||||
|
b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||||
|
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
// LB/RB
|
||||||
|
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
// Xbox button
|
||||||
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
||||||
|
|
||||||
|
// Triggers
|
||||||
|
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||||
|
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||||
|
|
||||||
|
// Left stick
|
||||||
|
leftStickX = buffer.getShort() / 32767.0f;
|
||||||
|
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
// Right stick
|
||||||
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
// Return true to send input
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendLedCommand(byte command) {
|
||||||
|
byte[] commandBuffer = {0x01, 0x03, command};
|
||||||
|
|
||||||
|
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
||||||
|
if (res != commandBuffer.length) {
|
||||||
|
LimeLog.warning("LED set transfer failed: "+res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doInit() {
|
||||||
|
// Turn the LED on corresponding to our device ID
|
||||||
|
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
||||||
|
|
||||||
|
// No need to fail init if the LED command fails
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class XboxOneController extends AbstractXboxController {
|
||||||
|
|
||||||
|
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||||
|
private static final int XB1_IFACE_PROTOCOL = 208;
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x24c6, // PowerA
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: odata_serial
|
||||||
|
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
|
|
||||||
|
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(device, connection, deviceId, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processButtons(ByteBuffer buffer) {
|
||||||
|
byte b = buffer.get();
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
|
||||||
|
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||||
|
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
||||||
|
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
||||||
|
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
leftTrigger = buffer.getShort() / 1023.0f;
|
||||||
|
rightTrigger = buffer.getShort() / 1023.0f;
|
||||||
|
|
||||||
|
leftStickX = buffer.getShort() / 32767.0f;
|
||||||
|
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ackModeReport(byte seqNum) {
|
||||||
|
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
|
switch (buffer.get())
|
||||||
|
{
|
||||||
|
case 0x20:
|
||||||
|
buffer.position(buffer.position()+3);
|
||||||
|
processButtons(buffer);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 0x07:
|
||||||
|
// The Xbox One S controller needs acks for mode reports otherwise
|
||||||
|
// it retransmits them forever.
|
||||||
|
if (buffer.get() == 0x30) {
|
||||||
|
ackModeReport(buffer.get());
|
||||||
|
buffer.position(buffer.position() + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
}
|
||||||
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (device.getVendorId() == supportedVid &&
|
||||||
|
device.getInterfaceCount() >= 1 &&
|
||||||
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||||
|
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doInit() {
|
||||||
|
// Send the initialization packet
|
||||||
|
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||||
|
if (res != XB1_INIT_DATA.length) {
|
||||||
|
LimeLog.warning("Initialization transfer failed: "+res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
|
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||||
|
|
||||||
|
private final EvdevListener listener;
|
||||||
|
private final String libraryPath;
|
||||||
|
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private InputStream evdevIn;
|
||||||
|
private OutputStream evdevOut;
|
||||||
|
private Process su;
|
||||||
|
private ServerSocket servSock;
|
||||||
|
private Socket evdevSock;
|
||||||
|
private Activity activity;
|
||||||
|
private boolean started = false;
|
||||||
|
|
||||||
|
private static final byte UNGRAB_REQUEST = 1;
|
||||||
|
private static final byte REGRAB_REQUEST = 2;
|
||||||
|
|
||||||
|
private final Thread handlerThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int deltaX = 0;
|
||||||
|
int deltaY = 0;
|
||||||
|
byte deltaScroll = 0;
|
||||||
|
|
||||||
|
// Bind a local listening socket for evdevreader to connect to
|
||||||
|
try {
|
||||||
|
servSock = new ServerSocket(0, 1);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String evdevReaderCmd = libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort();
|
||||||
|
|
||||||
|
// On Nougat and later, we'll need to pass the command directly to SU.
|
||||||
|
// Writing to SU's input stream after it has started doesn't seem to work anymore.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
// Launch evdev_reader directly via SU
|
||||||
|
try {
|
||||||
|
su = Runtime.getRuntime().exec("su -c "+evdevReaderCmd);
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Launch a SU shell on Marshmallow and earlier
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("su");
|
||||||
|
builder.redirectErrorStream(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
su = builder.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start evdevreader
|
||||||
|
DataOutputStream suOut = new DataOutputStream(su.getOutputStream());
|
||||||
|
try {
|
||||||
|
suOut.writeChars(evdevReaderCmd+"\n");
|
||||||
|
} catch (IOException e) {
|
||||||
|
reportDeviceNotRooted();
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for evdevreader's connection
|
||||||
|
LimeLog.info("Waiting for EvdevReader connection to port "+servSock.getLocalPort());
|
||||||
|
try {
|
||||||
|
evdevSock = servSock.accept();
|
||||||
|
evdevIn = evdevSock.getInputStream();
|
||||||
|
evdevOut = evdevSock.getOutputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LimeLog.info("EvdevReader connected from port "+evdevSock.getPort());
|
||||||
|
|
||||||
|
while (!isInterrupted() && !shutdown) {
|
||||||
|
EvdevEvent event;
|
||||||
|
try {
|
||||||
|
event = EvdevReader.read(evdevIn);
|
||||||
|
} catch (IOException e) {
|
||||||
|
event = null;
|
||||||
|
}
|
||||||
|
if (event == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
this.activity = activity;
|
||||||
|
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return LimelightBuildProps.ROOT_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportDeviceNotRooted() {
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(activity, "This device is not rooted - Mouse capture is unavailable", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
if (!started) {
|
||||||
|
// Start the handler thread if it's our first time
|
||||||
|
// capturing
|
||||||
|
handlerThread.start();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Send a request to regrab if we're already capturing
|
||||||
|
if (!shutdown && evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.write(REGRAB_REQUEST);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
if (started && !shutdown && evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.write(UNGRAB_REQUEST);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
// We need to stop the process in this context otherwise
|
||||||
|
// we could get stuck waiting on output from the process
|
||||||
|
// in order to terminate it.
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown = true;
|
||||||
|
handlerThread.interrupt();
|
||||||
|
|
||||||
|
if (servSock != null) {
|
||||||
|
try {
|
||||||
|
servSock.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evdevSock != null) {
|
||||||
|
try {
|
||||||
|
evdevSock.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evdevIn != null) {
|
||||||
|
try {
|
||||||
|
evdevIn.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evdevOut != null) {
|
||||||
|
try {
|
||||||
|
evdevOut.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (su != null) {
|
||||||
|
su.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
handlerThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,14 +24,10 @@ public class EvdevEvent {
|
|||||||
public static final short BTN_FORWARD = 0x115;
|
public static final short BTN_FORWARD = 0x115;
|
||||||
public static final short BTN_BACK = 0x116;
|
public static final short BTN_BACK = 0x116;
|
||||||
public static final short BTN_TASK = 0x117;
|
public static final short BTN_TASK = 0x117;
|
||||||
public static final short BTN_GAMEPAD = 0x130;
|
|
||||||
|
|
||||||
/* Keys */
|
public final short type;
|
||||||
public static final short KEY_Q = 16;
|
public final short code;
|
||||||
|
public final int value;
|
||||||
public short type;
|
|
||||||
public short code;
|
|
||||||
public int value;
|
|
||||||
|
|
||||||
public EvdevEvent(short type, short code, int value) {
|
public EvdevEvent(short type, short code, int value) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +1,58 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Locale;
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
public class EvdevReader {
|
public class EvdevReader {
|
||||||
static {
|
private static void readAll(InputStream in, ByteBuffer bb) throws IOException {
|
||||||
System.loadLibrary("evdev_reader");
|
byte[] buf = bb.array();
|
||||||
|
int ret;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
while (offset < buf.length) {
|
||||||
|
ret = in.read(buf, offset, buf.length-offset);
|
||||||
|
if (ret <= 0) {
|
||||||
|
throw new IOException("Read failed: "+ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requires root to chmod /dev/input/eventX
|
offset += ret;
|
||||||
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.
|
// Takes a byte buffer to use to read the output into.
|
||||||
// This buffer MUST be in native byte order and at least
|
// This buffer MUST be in native byte order and at least
|
||||||
// EVDEV_MAX_EVENT_SIZE bytes long.
|
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||||
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
public static EvdevEvent read(InputStream input) throws IOException {
|
||||||
int bytesRead = read(fd, buffer.array());
|
ByteBuffer bb;
|
||||||
if (bytesRead < 0) {
|
int packetLength;
|
||||||
LimeLog.warning("Failed to read: "+bytesRead);
|
|
||||||
return null;
|
// Read the packet length
|
||||||
}
|
bb = ByteBuffer.allocate(4).order(ByteOrder.nativeOrder());
|
||||||
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
readAll(input, bb);
|
||||||
LimeLog.warning("Short read: "+bytesRead);
|
packetLength = bb.getInt();
|
||||||
|
|
||||||
|
if (packetLength < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||||
|
LimeLog.warning("Short read: "+packetLength);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.limit(bytesRead);
|
// Read the rest of the packet
|
||||||
buffer.rewind();
|
bb = ByteBuffer.allocate(packetLength).order(ByteOrder.nativeOrder());
|
||||||
|
readAll(input, bb);
|
||||||
|
|
||||||
// Throw away the time stamp
|
// Throw away the time stamp
|
||||||
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
if (packetLength == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||||
buffer.getLong();
|
bb.getLong();
|
||||||
buffer.getLong();
|
bb.getLong();
|
||||||
} else {
|
} else {
|
||||||
buffer.getInt();
|
bb.getInt();
|
||||||
buffer.getInt();
|
bb.getInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
return new EvdevEvent(bb.getShort(), bb.getShort(), bb.getInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the fd from open()
|
|
||||||
public static native int close(int fd);
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
|
|||||||
|
|
||||||
public class EvdevTranslator {
|
public class EvdevTranslator {
|
||||||
|
|
||||||
public static final short EVDEV_KEY_CODES[] = {
|
private static final short[] EVDEV_KEY_CODES = {
|
||||||
0, //KeyEvent.VK_RESERVED
|
0, //KeyEvent.VK_RESERVED
|
||||||
KeyEvent.KEYCODE_ESCAPE,
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
KeyEvent.KEYCODE_1,
|
KeyEvent.KEYCODE_1,
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
||||||
|
*/
|
||||||
|
public class AnalogStick extends VirtualControllerElement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* outer radius size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_COMPLETE = 90;
|
||||||
|
/**
|
||||||
|
* analog stick size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
||||||
|
/**
|
||||||
|
* dead zone size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_DEADZONE = 90;
|
||||||
|
/**
|
||||||
|
* time frame for a double click
|
||||||
|
*/
|
||||||
|
public final static long timeoutDoubleClick = 350;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
||||||
|
*/
|
||||||
|
public final static long timeoutDeadzone = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface to update registered observers.
|
||||||
|
*/
|
||||||
|
public interface AnalogStickListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
||||||
|
*
|
||||||
|
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
||||||
|
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
||||||
|
*/
|
||||||
|
void onMovement(float x, float y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClick event will be fired on click on the analog stick
|
||||||
|
*/
|
||||||
|
void onClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
||||||
|
* stick.
|
||||||
|
*/
|
||||||
|
void onDoubleClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRevoke event will be fired on unpress of the analog stick.
|
||||||
|
*/
|
||||||
|
void onRevoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement states of the analog sick.
|
||||||
|
*/
|
||||||
|
private enum STICK_STATE {
|
||||||
|
NO_MOVEMENT,
|
||||||
|
MOVED_IN_DEAD_ZONE,
|
||||||
|
MOVED_ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click type states.
|
||||||
|
*/
|
||||||
|
private enum CLICK_STATE {
|
||||||
|
SINGLE,
|
||||||
|
DOUBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* configuration if the analog stick should be displayed as circle or square
|
||||||
|
*/
|
||||||
|
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* outer radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_complete = 0;
|
||||||
|
/**
|
||||||
|
* analog stick radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_analog_stick = 0;
|
||||||
|
/**
|
||||||
|
* dead zone radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_dead_zone = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* horizontal position in relation to the center of the element
|
||||||
|
*/
|
||||||
|
private float relative_x = 0;
|
||||||
|
/**
|
||||||
|
* vertical position in relation to the center of the element
|
||||||
|
*/
|
||||||
|
private float relative_y = 0;
|
||||||
|
|
||||||
|
|
||||||
|
private double movement_radius = 0;
|
||||||
|
private double movement_angle = 0;
|
||||||
|
|
||||||
|
private float position_stick_x = 0;
|
||||||
|
private float position_stick_y = 0;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
|
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
||||||
|
|
||||||
|
private List<AnalogStickListener> listeners = new ArrayList<>();
|
||||||
|
private long timeLastClick = 0;
|
||||||
|
|
||||||
|
private static double getMovementRadius(float x, float y) {
|
||||||
|
return Math.sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double getAngle(float way_x, float way_y) {
|
||||||
|
// prevent divisions by zero for corner cases
|
||||||
|
if (way_x == 0) {
|
||||||
|
return way_y < 0 ? Math.PI : 0;
|
||||||
|
} else if (way_y == 0) {
|
||||||
|
if (way_x > 0) {
|
||||||
|
return Math.PI * 3 / 2;
|
||||||
|
} else if (way_x < 0) {
|
||||||
|
return Math.PI * 1 / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return correct calculated angle for each quadrant
|
||||||
|
if (way_x > 0) {
|
||||||
|
if (way_y < 0) {
|
||||||
|
// first quadrant
|
||||||
|
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
||||||
|
} else {
|
||||||
|
// second quadrant
|
||||||
|
return Math.PI + Math.atan((double) (way_x / way_y));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (way_y > 0) {
|
||||||
|
// third quadrant
|
||||||
|
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
||||||
|
} else {
|
||||||
|
// fourth quadrant
|
||||||
|
return 0 + Math.atan((double) (-way_x / -way_y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnalogStick(VirtualController controller, Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
// reset stick position
|
||||||
|
position_stick_x = getWidth() / 2;
|
||||||
|
position_stick_y = getHeight() / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAnalogStickListener(AnalogStickListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnMovement(float x, float y) {
|
||||||
|
_DBG("movement x: " + x + " movement y: " + y);
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onMovement(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnClick() {
|
||||||
|
_DBG("click");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnDoubleClick() {
|
||||||
|
_DBG("double click");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onDoubleClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnRevoke() {
|
||||||
|
_DBG("revoke");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onRevoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
// calculate new radius sizes depending
|
||||||
|
radius_complete = getPercent(getCorrectWidth() / 2, 90);
|
||||||
|
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||||
|
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||||
|
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
// draw outer circle
|
||||||
|
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
} else {
|
||||||
|
paint.setColor(pressedColor);
|
||||||
|
}
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
||||||
|
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
// draw dead zone
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
||||||
|
|
||||||
|
// draw stick depending on state
|
||||||
|
switch (stick_state) {
|
||||||
|
case NO_MOVEMENT: {
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MOVED_IN_DEAD_ZONE:
|
||||||
|
case MOVED_ACTIVE: {
|
||||||
|
paint.setColor(pressedColor);
|
||||||
|
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition() {
|
||||||
|
// get 100% way
|
||||||
|
float complete = radius_complete - radius_analog_stick;
|
||||||
|
|
||||||
|
// calculate relative way
|
||||||
|
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||||
|
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||||
|
|
||||||
|
// update positions
|
||||||
|
position_stick_x = getWidth() / 2 - correlated_x;
|
||||||
|
position_stick_y = getHeight() / 2 - correlated_y;
|
||||||
|
|
||||||
|
// Stay active even if we're back in the deadzone because we know the user is actively
|
||||||
|
// giving analog stick input and we don't want to snap back into the deadzone.
|
||||||
|
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||||
|
// them to make precise movements.
|
||||||
|
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||||
|
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||||
|
movement_radius > radius_dead_zone) ?
|
||||||
|
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
|
||||||
|
// trigger move event if state active
|
||||||
|
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
||||||
|
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// save last click state
|
||||||
|
CLICK_STATE lastClickState = click_state;
|
||||||
|
|
||||||
|
// get absolute way for each axis
|
||||||
|
relative_x = -(getWidth() / 2 - event.getX());
|
||||||
|
relative_y = -(getHeight() / 2 - event.getY());
|
||||||
|
|
||||||
|
// get radius and angel of movement from center
|
||||||
|
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||||
|
movement_angle = getAngle(relative_x, relative_y);
|
||||||
|
|
||||||
|
// chop radius if out of outer circle and already pressed
|
||||||
|
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||||
|
// not pressed already, so ignore event from outer circle
|
||||||
|
if (!isPressed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
movement_radius = radius_complete - radius_analog_stick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle event depending on action
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
// down event (touch event)
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
// set to dead zoned, will be corrected in update position if necessary
|
||||||
|
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
// check for double click
|
||||||
|
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||||
|
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
||||||
|
click_state = CLICK_STATE.DOUBLE;
|
||||||
|
notifyOnDoubleClick();
|
||||||
|
} else {
|
||||||
|
click_state = CLICK_STATE.SINGLE;
|
||||||
|
notifyOnClick();
|
||||||
|
}
|
||||||
|
// reset last click timestamp
|
||||||
|
timeLastClick = System.currentTimeMillis();
|
||||||
|
// set item pressed and update
|
||||||
|
setPressed(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// up event (revoke touch)
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
setPressed(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed()) {
|
||||||
|
// when is pressed calculate new positions (will trigger movement if necessary)
|
||||||
|
updatePosition();
|
||||||
|
} else {
|
||||||
|
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
|
notifyOnRevoke();
|
||||||
|
// not longer pressed reset analog stick
|
||||||
|
notifyOnMovement(0, 0);
|
||||||
|
}
|
||||||
|
// refresh view
|
||||||
|
invalidate();
|
||||||
|
// accept the touch event
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a digital button on screen element. It is used to get click and double click user input.
|
||||||
|
*/
|
||||||
|
public class DigitalButton extends VirtualControllerElement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface to update registered observers.
|
||||||
|
*/
|
||||||
|
public interface DigitalButtonListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClick event will be fired on button click.
|
||||||
|
*/
|
||||||
|
void onClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLongClick event will be fired on button long click.
|
||||||
|
*/
|
||||||
|
void onLongClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRelease event will be fired on button unpress.
|
||||||
|
*/
|
||||||
|
void onRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private class TimerLongClickTimerTask extends TimerTask {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
onLongClickCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
||||||
|
private String text = "";
|
||||||
|
private int icon = -1;
|
||||||
|
private long timerLongClickTimeout = 3000;
|
||||||
|
private Timer timerLongClick = null;
|
||||||
|
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private int layer;
|
||||||
|
private DigitalButton movingButton = null;
|
||||||
|
|
||||||
|
boolean inRange(float x, float y) {
|
||||||
|
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
|
||||||
|
(this.getY() < y && this.getY() + this.getHeight() > y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
|
||||||
|
// check if the movement happened in the same layer
|
||||||
|
if (movingButton.layer != this.layer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save current pressed state
|
||||||
|
boolean wasPressed = isPressed();
|
||||||
|
|
||||||
|
// check if the movement directly happened on the button
|
||||||
|
if ((this.movingButton == null || movingButton == this.movingButton)
|
||||||
|
&& this.inRange(x, y)) {
|
||||||
|
// set button pressed state depending on moving button pressed state
|
||||||
|
if (this.isPressed() != movingButton.isPressed()) {
|
||||||
|
this.setPressed(movingButton.isPressed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if the movement is outside of the range and the movement button
|
||||||
|
// is the saved moving button
|
||||||
|
else if (movingButton == this.movingButton) {
|
||||||
|
this.setPressed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a change occurred
|
||||||
|
if (wasPressed != isPressed()) {
|
||||||
|
if (isPressed()) {
|
||||||
|
// is pressed set moving button and emit click event
|
||||||
|
this.movingButton = movingButton;
|
||||||
|
onClickCallback();
|
||||||
|
} else {
|
||||||
|
// no longer pressed reset moving button and emit release event
|
||||||
|
this.movingButton = null;
|
||||||
|
onReleaseCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkMovementForAllButtons(float x, float y) {
|
||||||
|
for (VirtualControllerElement element : virtualController.getElements()) {
|
||||||
|
if (element != this && element instanceof DigitalButton) {
|
||||||
|
((DigitalButton) element).checkMovement(x, y, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DigitalButton(VirtualController controller, int layer, Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
this.layer = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setText(String text) {
|
||||||
|
this.text = text;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(int id) {
|
||||||
|
this.icon = id;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setTextSize(getPercent(getWidth(), 30));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||||
|
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
||||||
|
|
||||||
|
if (icon != -1) {
|
||||||
|
Drawable d = getResources().getDrawable(icon);
|
||||||
|
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
||||||
|
d.draw(canvas);
|
||||||
|
} else {
|
||||||
|
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
||||||
|
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickCallback() {
|
||||||
|
_DBG("clicked");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
timerLongClick = new Timer();
|
||||||
|
longClickTimerTask = new TimerLongClickTimerTask();
|
||||||
|
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLongClickCallback() {
|
||||||
|
_DBG("long click");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onLongClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onReleaseCallback() {
|
||||||
|
_DBG("released");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onRelease();
|
||||||
|
}
|
||||||
|
timerLongClick.cancel();
|
||||||
|
longClickTimerTask.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// get masked (not specific to a pointer) action
|
||||||
|
float x = getX() + event.getX();
|
||||||
|
float y = getY() + event.getY();
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
movingButton = null;
|
||||||
|
setPressed(true);
|
||||||
|
onClickCallback();
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
checkMovementForAllButtons(x, y);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
setPressed(false);
|
||||||
|
onReleaseCallback();
|
||||||
|
|
||||||
|
checkMovementForAllButtons(x, y);
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DigitalPad extends VirtualControllerElement {
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
|
||||||
|
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
||||||
|
List<DigitalPadListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final int DPAD_MARGIN = 5;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
public DigitalPad(VirtualController controller, Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||||
|
// draw no direction rect
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
||||||
|
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw left rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||||
|
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// draw up rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw right rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||||
|
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw down rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||||
|
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw left up line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||||
|
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw up right line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw right down line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
||||||
|
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw down left line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newDirectionCallback(int direction) {
|
||||||
|
_DBG("direction: " + direction);
|
||||||
|
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalPadListener listener : listeners) {
|
||||||
|
listener.onDirectionChange(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// get masked (not specific to a pointer) action
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
direction = 0;
|
||||||
|
|
||||||
|
if (event.getX() < getPercent(getWidth(), 33)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
||||||
|
}
|
||||||
|
if (event.getX() > getPercent(getWidth(), 66)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
||||||
|
}
|
||||||
|
if (event.getY() > getPercent(getHeight(), 66)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
||||||
|
}
|
||||||
|
if (event.getY() < getPercent(getHeight(), 33)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_UP;
|
||||||
|
}
|
||||||
|
newDirectionCallback(direction);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
direction = 0;
|
||||||
|
newDirectionCallback(direction);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface DigitalPadListener {
|
||||||
|
void onDirectionChange(int direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class LeftAnalogStick extends AnalogStick {
|
||||||
|
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
|
||||||
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
|
@Override
|
||||||
|
public void onMovement(float x, float y) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftStickX = (short) (x * 0x7FFE);
|
||||||
|
inputContext.leftStickY = (short) (y * 0x7FFE);
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoubleClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevoke() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class LeftTrigger extends DigitalButton {
|
||||||
|
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
|
super(controller, layer, context);
|
||||||
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftTrigger = (byte) 0xFF;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftTrigger = (byte) 0x00;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class RightAnalogStick extends AnalogStick {
|
||||||
|
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||||
|
super(controller, context);
|
||||||
|
|
||||||
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
|
@Override
|
||||||
|
public void onMovement(float x, float y) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightStickX = (short) (x * 0x7FFE);
|
||||||
|
inputContext.rightStickY = (short) (y * 0x7FFE);
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoubleClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevoke() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class RightTrigger extends DigitalButton {
|
||||||
|
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
|
super(controller, layer, context);
|
||||||
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightTrigger = (byte) 0xFF;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightTrigger = (byte) 0x00;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class VirtualController {
|
||||||
|
public class ControllerInputContext {
|
||||||
|
public short inputMap = 0x0000;
|
||||||
|
public byte leftTrigger = 0x00;
|
||||||
|
public byte rightTrigger = 0x00;
|
||||||
|
public short rightStickX = 0x0000;
|
||||||
|
public short rightStickY = 0x0000;
|
||||||
|
public short leftStickX = 0x0000;
|
||||||
|
public short leftStickY = 0x0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ControllerMode {
|
||||||
|
Active,
|
||||||
|
Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
|
private NvConnection connection = null;
|
||||||
|
private Context context = null;
|
||||||
|
|
||||||
|
private FrameLayout frame_layout = null;
|
||||||
|
private RelativeLayout relative_layout = null;
|
||||||
|
|
||||||
|
ControllerMode currentMode = ControllerMode.Active;
|
||||||
|
ControllerInputContext inputContext = new ControllerInputContext();
|
||||||
|
|
||||||
|
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
||||||
|
private Button buttonConfigure = null;
|
||||||
|
|
||||||
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
|
|
||||||
|
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||||
|
this.connection = conn;
|
||||||
|
this.frame_layout = layout;
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
relative_layout = new RelativeLayout(context);
|
||||||
|
|
||||||
|
frame_layout.addView(relative_layout);
|
||||||
|
|
||||||
|
buttonConfigure = new Button(context);
|
||||||
|
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||||
|
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String message;
|
||||||
|
|
||||||
|
if (currentMode == ControllerMode.Configuration) {
|
||||||
|
currentMode = ControllerMode.Active;
|
||||||
|
message = "Exiting configuration mode";
|
||||||
|
} else {
|
||||||
|
currentMode = ControllerMode.Configuration;
|
||||||
|
message = "Entering configuration mode";
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
relative_layout.invalidate();
|
||||||
|
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeElements() {
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
relative_layout.removeView(element);
|
||||||
|
}
|
||||||
|
elements.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||||
|
elements.add(element);
|
||||||
|
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||||
|
layoutParams.setMargins(x, y, 0, 0);
|
||||||
|
|
||||||
|
relative_layout.addView(element, layoutParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VirtualControllerElement> getElements() {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final void _DBG(String text) {
|
||||||
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
|
System.out.println("VirtualController: " + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshLayout() {
|
||||||
|
relative_layout.removeAllViews();
|
||||||
|
removeElements();
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
|
int buttonSize = (int)(screen.heightPixels*0.05f);
|
||||||
|
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||||
|
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
|
||||||
|
|
||||||
|
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerMode getControllerMode() {
|
||||||
|
return currentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerInputContext getControllerInputContext() {
|
||||||
|
return inputContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInputContext() {
|
||||||
|
sendControllerInputPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControllerInputPacket() {
|
||||||
|
try {
|
||||||
|
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||||
|
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||||
|
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||||
|
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||||
|
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||||
|
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||||
|
|
||||||
|
if (connection != null) {
|
||||||
|
connection.sendControllerInput(
|
||||||
|
inputContext.inputMap,
|
||||||
|
inputContext.leftTrigger,
|
||||||
|
inputContext.rightTrigger,
|
||||||
|
inputContext.leftStickX,
|
||||||
|
inputContext.leftStickY,
|
||||||
|
inputContext.rightStickX,
|
||||||
|
inputContext.rightStickY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+285
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class VirtualControllerConfigurationLoader {
|
||||||
|
private static final String PROFILE_PATH = "profiles";
|
||||||
|
|
||||||
|
private static int getPercent(
|
||||||
|
int percent,
|
||||||
|
int total) {
|
||||||
|
return (int) (((float) total / (float) 100) * (float) percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalPad createDigitalPad(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
|
||||||
|
DigitalPad digitalPad = new DigitalPad(controller, context);
|
||||||
|
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
||||||
|
@Override
|
||||||
|
public void onDirectionChange(int direction) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
|
||||||
|
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
}
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return digitalPad;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createDigitalButton(
|
||||||
|
final int keyShort,
|
||||||
|
final int keyLong,
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
DigitalButton button = new DigitalButton(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
|
||||||
|
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= keyShort;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= keyLong;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~keyShort;
|
||||||
|
inputContext.inputMap &= ~keyLong;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createLeftTrigger(
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createRightTrigger(
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
RightTrigger button = new RightTrigger(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalogStick createLeftStick(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
return new LeftAnalogStick(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalogStick createRightStick(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
return new RightAnalogStick(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int BUTTON_BASE_X = 65;
|
||||||
|
private static final int BUTTON_BASE_Y = 5;
|
||||||
|
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
||||||
|
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
||||||
|
|
||||||
|
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
|
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||||
|
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||||
|
|
||||||
|
controller.addElement(createDigitalPad(controller, context),
|
||||||
|
getPercent(5, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(30, screen.widthPixels),
|
||||||
|
getPercent(40, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftTrigger(
|
||||||
|
0, "LT", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightTrigger(
|
||||||
|
0, "RT", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftStick(controller, context),
|
||||||
|
getPercent(5, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightStick(controller, context),
|
||||||
|
getPercent(55, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(90, screen.heightPixels),
|
||||||
|
getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||||
|
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(90, screen.heightPixels),
|
||||||
|
getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOT IMPLEMENTED YET,
|
||||||
|
this should later be used to store and load a profile for the virtual controller
|
||||||
|
public static void saveProfile(final String name,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
|
||||||
|
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
|
||||||
|
name, Activity.MODE_PRIVATE);
|
||||||
|
|
||||||
|
JSONArray elementConfigurations = new JSONArray();
|
||||||
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
|
JSONObject elementConfiguration = new JSONObject();
|
||||||
|
try {
|
||||||
|
elementConfiguration.put("TYPE", element.getClass().getName());
|
||||||
|
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
|
||||||
|
elementConfigurations.put(elementConfiguration);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPreferences.Editor editor= preferences.edit();
|
||||||
|
editor.putString("ELEMENTS", elementConfigurations.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadFromPreferences(final VirtualController controller) {
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
+290
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
public abstract class VirtualControllerElement extends View {
|
||||||
|
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
|
protected VirtualController virtualController;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private int normalColor = 0xF0888888;
|
||||||
|
protected int pressedColor = 0xF00000FF;
|
||||||
|
private int configNormalColor = 0xF0FF0000;
|
||||||
|
private int configSelectedColor = 0xF000FF00;
|
||||||
|
|
||||||
|
protected int startSize_x;
|
||||||
|
protected int startSize_y;
|
||||||
|
|
||||||
|
float position_pressed_x = 0;
|
||||||
|
float position_pressed_y = 0;
|
||||||
|
|
||||||
|
private enum Mode {
|
||||||
|
Normal,
|
||||||
|
Resize,
|
||||||
|
Move
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mode currentMode = Mode.Normal;
|
||||||
|
|
||||||
|
protected VirtualControllerElement(VirtualController controller, Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
this.virtualController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||||
|
int newPos_x = (int) getX() + x - pressed_x;
|
||||||
|
int newPos_y = (int) getY() + y - pressed_y;
|
||||||
|
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
||||||
|
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
||||||
|
layoutParams.rightMargin = 0;
|
||||||
|
layoutParams.bottomMargin = 0;
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
int newHeight = height + (startSize_y - pressed_y);
|
||||||
|
int newWidth = width + (startSize_x - pressed_x);
|
||||||
|
|
||||||
|
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
||||||
|
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
onElementDraw(canvas);
|
||||||
|
|
||||||
|
if (currentMode != Mode.Normal) {
|
||||||
|
paint.setColor(configSelectedColor);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
|
||||||
|
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||||
|
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
||||||
|
paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
protected void actionShowNormalColorChooser() {
|
||||||
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(AmbilWarnaDialog dialog)
|
||||||
|
{}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
|
normalColor = color;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
colorDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionShowPressedColorChooser() {
|
||||||
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(AmbilWarnaDialog dialog) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
|
pressedColor = color;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
colorDialog.show();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected void actionEnableMove() {
|
||||||
|
currentMode = Mode.Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionEnableResize() {
|
||||||
|
currentMode = Mode.Resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionCancel() {
|
||||||
|
currentMode = Mode.Normal;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDefaultColor() {
|
||||||
|
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||||
|
configNormalColor : normalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDefaultStrokeWidth() {
|
||||||
|
DisplayMetrics screen = getResources().getDisplayMetrics();
|
||||||
|
return (int)(screen.heightPixels*0.004f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void showConfigurationDialog() {
|
||||||
|
try {
|
||||||
|
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||||
|
|
||||||
|
alertBuilder.setTitle("Configuration");
|
||||||
|
|
||||||
|
CharSequence functions[] = new CharSequence[]{
|
||||||
|
"Move",
|
||||||
|
"Resize",
|
||||||
|
/*election
|
||||||
|
"Set n
|
||||||
|
Disable color sormal color",
|
||||||
|
"Set pressed color",
|
||||||
|
*/
|
||||||
|
"Cancel"
|
||||||
|
};
|
||||||
|
|
||||||
|
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which) {
|
||||||
|
case 0: { // move
|
||||||
|
actionEnableMove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: { // resize
|
||||||
|
actionEnableResize();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
case 2: { // set default color
|
||||||
|
actionShowNormalColorChooser();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: { // set pressed color
|
||||||
|
actionShowPressedColorChooser();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
default: { // cancel
|
||||||
|
actionCancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AlertDialog alert = alertBuilder.create();
|
||||||
|
// show menu
|
||||||
|
alert.show();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
||||||
|
return onElementTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
position_pressed_x = event.getX();
|
||||||
|
position_pressed_y = event.getY();
|
||||||
|
startSize_x = getWidth();
|
||||||
|
startSize_y = getHeight();
|
||||||
|
|
||||||
|
actionEnableMove();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
switch (currentMode) {
|
||||||
|
case Move: {
|
||||||
|
moveElement(
|
||||||
|
(int) position_pressed_x,
|
||||||
|
(int) position_pressed_y,
|
||||||
|
(int) event.getX(),
|
||||||
|
(int) event.getY());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Resize: {
|
||||||
|
resizeElement(
|
||||||
|
(int) position_pressed_x,
|
||||||
|
(int) position_pressed_y,
|
||||||
|
(int) event.getX(),
|
||||||
|
(int) event.getY());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Normal: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
actionCancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected void onElementDraw(Canvas canvas);
|
||||||
|
|
||||||
|
abstract public boolean onElementTouchEvent(MotionEvent event);
|
||||||
|
|
||||||
|
protected static final void _DBG(String text) {
|
||||||
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
|
System.out.println(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColors(int normalColor, int pressedColor) {
|
||||||
|
this.normalColor = normalColor;
|
||||||
|
this.pressedColor = pressedColor;
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final float getPercent(float value, float percent) {
|
||||||
|
return value / 100 * percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final int getCorrectWidth() {
|
||||||
|
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
public JSONObject getConfiguration () {
|
||||||
|
JSONObject configuration = new JSONObject();
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadConfiguration (JSONObject configuration) {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.concurrent.locks.LockSupport;
|
|
||||||
|
|
||||||
import android.graphics.PixelFormat;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.SurfaceHolder;
|
|
||||||
|
|
||||||
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 com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
|
||||||
|
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
|
||||||
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
|
||||||
|
|
||||||
private Thread rendererThread;
|
|
||||||
private int targetFps;
|
|
||||||
|
|
||||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
|
||||||
private ByteBuffer decoderBuffer;
|
|
||||||
|
|
||||||
// Only sleep if the difference is above this value
|
|
||||||
private static final int WAIT_CEILING_MS = 8;
|
|
||||||
|
|
||||||
private static final int LOW_PERF = 1;
|
|
||||||
private static final int MED_PERF = 2;
|
|
||||||
private static final int HIGH_PERF = 3;
|
|
||||||
|
|
||||||
private int totalFrames;
|
|
||||||
private long totalTimeMs;
|
|
||||||
|
|
||||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private int findOptimalPerformanceLevel() {
|
|
||||||
StringBuilder cpuInfo = new StringBuilder();
|
|
||||||
BufferedReader br = null;
|
|
||||||
try {
|
|
||||||
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
|
||||||
for (;;) {
|
|
||||||
int ch = br.read();
|
|
||||||
if (ch == -1)
|
|
||||||
break;
|
|
||||||
cpuInfo.append((char)ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we're doing very simple heuristics based on CPU model
|
|
||||||
String cpuInfoStr = cpuInfo.toString();
|
|
||||||
|
|
||||||
// We order them from greatest to least for proper detection
|
|
||||||
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
|
||||||
// TODO Make this better (only even kind of works on ARM)
|
|
||||||
if (Build.FINGERPRINT.contains("generic")) {
|
|
||||||
// Emulator
|
|
||||||
return LOW_PERF;
|
|
||||||
}
|
|
||||||
else if (cpuInfoStr.contains("0xc0f")) {
|
|
||||||
// Cortex-A15
|
|
||||||
return MED_PERF;
|
|
||||||
}
|
|
||||||
else if (cpuInfoStr.contains("0xc09")) {
|
|
||||||
// Cortex-A9
|
|
||||||
return LOW_PERF;
|
|
||||||
}
|
|
||||||
else if (cpuInfoStr.contains("0xc07")) {
|
|
||||||
// Cortex-A7
|
|
||||||
return LOW_PERF;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Didn't have anything we're looking for
|
|
||||||
return MED_PERF;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
} finally {
|
|
||||||
if (br != null) {
|
|
||||||
try {
|
|
||||||
br.close();
|
|
||||||
} catch (IOException e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't read cpuinfo, so assume medium
|
|
||||||
return MED_PERF;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
|
||||||
this.targetFps = redrawRate;
|
|
||||||
|
|
||||||
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
|
||||||
int threadCount;
|
|
||||||
|
|
||||||
int avcFlags = 0;
|
|
||||||
switch (perfLevel) {
|
|
||||||
case HIGH_PERF:
|
|
||||||
// Single threaded low latency decode is ideal but hard to acheive
|
|
||||||
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
|
|
||||||
threadCount = 1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LOW_PERF:
|
|
||||||
// Disable the loop filter for performance reasons
|
|
||||||
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
|
|
||||||
AvcDecoder.FAST_BILINEAR_FILTERING |
|
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// Use plenty of threads to try to utilize the CPU as best we can
|
|
||||||
threadCount = cpuCount - 1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
case MED_PERF:
|
|
||||||
avcFlags = AvcDecoder.BILINEAR_FILTERING |
|
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// Only use 2 threads to minimize frame processing latency
|
|
||||||
threadCount = 2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user wants quality, we'll remove the low IQ flags
|
|
||||||
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
|
||||||
// Make sure the loop filter is enabled
|
|
||||||
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
|
||||||
|
|
||||||
// Disable the non-compliant speed optimizations
|
|
||||||
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
LimeLog.info("Using high quality decoding");
|
|
||||||
}
|
|
||||||
|
|
||||||
SurfaceHolder sh = (SurfaceHolder)renderTarget;
|
|
||||||
sh.setFormat(PixelFormat.RGBX_8888);
|
|
||||||
|
|
||||||
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
|
||||||
if (err != 0) {
|
|
||||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvcDecoder.setRenderTarget(sh.getSurface());
|
|
||||||
|
|
||||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
|
||||||
|
|
||||||
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean start(final VideoDepacketizer depacketizer) {
|
|
||||||
rendererThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
|
||||||
DecodeUnit du;
|
|
||||||
while (!isInterrupted())
|
|
||||||
{
|
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
|
||||||
if (du != null) {
|
|
||||||
submitDecodeUnit(du);
|
|
||||||
depacketizer.freeDecodeUnit(du);
|
|
||||||
}
|
|
||||||
|
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
|
||||||
LockSupport.parkNanos(1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
|
||||||
AvcDecoder.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
rendererThread.setName("Video - Renderer (CPU)");
|
|
||||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
|
||||||
rendererThread.start();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long computePresentationTimeMs(int frameRate) {
|
|
||||||
return System.currentTimeMillis() + (1000 / frameRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
rendererThread.interrupt();
|
|
||||||
|
|
||||||
try {
|
|
||||||
rendererThread.join();
|
|
||||||
} catch (InterruptedException e) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
AvcDecoder.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
|
||||||
byte[] data;
|
|
||||||
|
|
||||||
// Use the reserved decoder buffer if this decode unit will fit
|
|
||||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
|
||||||
decoderBuffer.clear();
|
|
||||||
|
|
||||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
|
||||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
data = decoderBuffer.array();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
|
||||||
|
|
||||||
int offset = 0;
|
|
||||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
|
||||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
|
||||||
offset += bbd.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
|
||||||
|
|
||||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|
||||||
|
|
||||||
private VideoDecoderRenderer decoderRenderer;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
if (decoderRenderer != null) {
|
|
||||||
decoderRenderer.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
|
||||||
if (decoderRenderer == null) {
|
|
||||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
|
||||||
}
|
|
||||||
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initializeWithFlags(int drFlags) {
|
|
||||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
|
||||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
|
||||||
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
decoderRenderer = new AndroidCpuDecoderRenderer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isHardwareAccelerated() {
|
|
||||||
if (decoderRenderer == null) {
|
|
||||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
|
||||||
}
|
|
||||||
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean start(VideoDepacketizer depacketizer) {
|
|
||||||
return decoderRenderer.start(depacketizer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
decoderRenderer.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCapabilities() {
|
|
||||||
return decoderRenderer.getCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getAverageDecoderLatency() {
|
|
||||||
if (decoderRenderer != null) {
|
|
||||||
return decoderRenderer.getAverageDecoderLatency();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getAverageEndToEndLatency() {
|
|
||||||
if (decoderRenderer != null) {
|
|
||||||
return decoderRenderer.getAverageEndToEndLatency();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
|
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
|
||||||
|
public abstract boolean isHevcSupported();
|
||||||
|
public abstract boolean isAvcSupported();
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
|
|
||||||
|
import org.jcodec.codecs.h264.H264Utils;
|
||||||
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||||
|
|
||||||
@@ -12,8 +13,8 @@ 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.VideoDepacketizer;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
@@ -22,76 +23,177 @@ import android.media.MediaCodec.CodecException;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|
||||||
|
private static final boolean USE_FRAME_RENDER_TIME = false;
|
||||||
|
|
||||||
|
// Used on versions < 5.0
|
||||||
|
private ByteBuffer[] legacyInputBuffers;
|
||||||
|
|
||||||
|
private MediaCodecInfo avcDecoder;
|
||||||
|
private MediaCodecInfo hevcDecoder;
|
||||||
|
|
||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private VideoDepacketizer depacketizer;
|
private VideoDepacketizer depacketizer;
|
||||||
private boolean adaptivePlayback;
|
private boolean adaptivePlayback, directSubmit;
|
||||||
|
private boolean constrainedHighProfile;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
|
private VideoFormat videoFormat;
|
||||||
|
|
||||||
|
private boolean needsBaselineSpsHack;
|
||||||
|
private SeqParameterSet savedSps;
|
||||||
|
|
||||||
private long lastTimestampUs;
|
private long lastTimestampUs;
|
||||||
private long totalTimeMs;
|
private long totalTimeMs;
|
||||||
private long decoderTimeMs;
|
private long decoderTimeMs;
|
||||||
private int totalFrames;
|
private int totalFrames;
|
||||||
|
|
||||||
private String decoderName;
|
|
||||||
private int numSpsIn;
|
private int numSpsIn;
|
||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
|
private int numVpsIn;
|
||||||
private int numIframeIn;
|
private int numIframeIn;
|
||||||
|
|
||||||
private static final boolean ENABLE_ASYNC_RENDERER = false;
|
private MediaCodecInfo findAvcDecoder() {
|
||||||
|
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
|
||||||
|
if (decoder == null) {
|
||||||
|
decoder = MediaCodecHelper.findFirstDecoder("video/avc");
|
||||||
|
}
|
||||||
|
return decoder;
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
private MediaCodecInfo findHevcDecoder(int videoFormat) {
|
||||||
public MediaCodecDecoderRenderer() {
|
// Don't return anything if H.265 is forced off
|
||||||
|
if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't try the first HEVC decoder. We'd rather fall back to hardware accelerated AVC instead
|
||||||
|
//
|
||||||
|
// We need HEVC Main profile, so we could pass that constant to findProbableSafeDecoder, however
|
||||||
|
// some decoders (at least Qualcomm's Snapdragon 805) don't properly report support
|
||||||
|
// for even required levels of HEVC.
|
||||||
|
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||||
|
if (decoderInfo != null) {
|
||||||
|
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) {
|
||||||
|
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
|
||||||
|
|
||||||
|
if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) {
|
||||||
|
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoderInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaCodecDecoderRenderer(int videoFormat) {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
|
avcDecoder = findAvcDecoder();
|
||||||
if (decoder == null) {
|
if (avcDecoder != null) {
|
||||||
decoder = MediaCodecHelper.findFirstDecoder();
|
LimeLog.info("Selected AVC decoder: "+avcDecoder.getName());
|
||||||
}
|
}
|
||||||
if (decoder == null) {
|
else {
|
||||||
// This case is handled later in setup()
|
LimeLog.warning("No AVC decoder found");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decoderName = decoder.getName();
|
hevcDecoder = findHevcDecoder(videoFormat);
|
||||||
|
if (hevcDecoder != null) {
|
||||||
|
LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("No HEVC decoder found");
|
||||||
|
}
|
||||||
|
|
||||||
// Set decoder-specific attributes
|
// Set attributes that are queried in getCapabilities(). This must be done here
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
// because getCapabilities() may be called before setup() in current versions of the common
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
// library. The limitation of this is that we don't know whether we're using HEVC or AVC, so
|
||||||
if (needsSpsBitstreamFixup) {
|
// we just assume AVC. This isn't really a problem because the capabilities are usually
|
||||||
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
// shared between AVC and HEVC decoders on the same device.
|
||||||
|
if (avcDecoder != null) {
|
||||||
|
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
|
||||||
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder.getName());
|
||||||
|
|
||||||
|
if (directSubmit) {
|
||||||
|
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
|
||||||
}
|
}
|
||||||
isExynos4 = MediaCodecHelper.isExynos4Device();
|
|
||||||
if (isExynos4) {
|
|
||||||
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean isHevcSupported() {
|
||||||
|
return hevcDecoder != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvcSupported() {
|
||||||
|
return avcDecoder != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setup(VideoDecoderRenderer.VideoFormat format, int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.initialWidth = width;
|
this.initialWidth = width;
|
||||||
this.initialHeight = height;
|
this.initialHeight = height;
|
||||||
|
this.videoFormat = format;
|
||||||
|
|
||||||
if (decoderName == null) {
|
String mimeType;
|
||||||
LimeLog.severe("No available hardware decoder!");
|
String selectedDecoderName;
|
||||||
|
|
||||||
|
if (videoFormat == VideoFormat.H264) {
|
||||||
|
mimeType = "video/avc";
|
||||||
|
selectedDecoderName = avcDecoder.getName();
|
||||||
|
|
||||||
|
if (avcDecoder == null) {
|
||||||
|
LimeLog.severe("No available AVC decoder!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These fixups only apply to H264 decoders
|
||||||
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderName);
|
||||||
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderName);
|
||||||
|
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(selectedDecoderName);
|
||||||
|
isExynos4 = MediaCodecHelper.isExynos4Device();
|
||||||
|
if (needsSpsBitstreamFixup) {
|
||||||
|
LimeLog.info("Decoder "+selectedDecoderName+" needs SPS bitstream restrictions fixup");
|
||||||
|
}
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Decoder "+selectedDecoderName+" needs baseline SPS hack");
|
||||||
|
}
|
||||||
|
if (constrainedHighProfile) {
|
||||||
|
LimeLog.info("Decoder "+selectedDecoderName+" needs constrained high profile");
|
||||||
|
}
|
||||||
|
if (isExynos4) {
|
||||||
|
LimeLog.info("Decoder "+selectedDecoderName+" is on Exynos 4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (videoFormat == VideoFormat.H265) {
|
||||||
|
mimeType = "video/hevc";
|
||||||
|
selectedDecoderName = hevcDecoder.getName();
|
||||||
|
|
||||||
|
if (hevcDecoder == null) {
|
||||||
|
LimeLog.severe("No available HEVC decoder!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Unknown format
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Codecs have been known to throw all sorts of crazy runtime exceptions
|
// Codecs have been known to throw all sorts of crazy runtime exceptions
|
||||||
// due to implementation problems
|
// due to implementation problems
|
||||||
try {
|
try {
|
||||||
videoDecoder = MediaCodec.createByCodecName(decoderName);
|
videoDecoder = MediaCodec.createByCodecName(selectedDecoderName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
|
||||||
|
|
||||||
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
||||||
// so we don't fill these pre-KitKat
|
// so we don't fill these pre-KitKat
|
||||||
@@ -100,62 +202,37 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
// Operate at maximum rate to lower latency as much as possible on
|
||||||
videoDecoder.setCallback(new MediaCodec.Callback() {
|
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
||||||
@Override
|
// but that will actually result in the decoder crashing if it can't satisfy
|
||||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
// our (ludicrous) operating rate requirement.
|
||||||
LimeLog.info("Output format changed");
|
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
||||||
LimeLog.info("New output Format: " + format);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOutputBufferAvailable(MediaCodec codec, int index,
|
|
||||||
BufferInfo info) {
|
|
||||||
try {
|
|
||||||
// FIXME: It looks like we can't frameskip here
|
|
||||||
codec.releaseOutputBuffer(index, true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
|
||||||
try {
|
|
||||||
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// What do we do here?
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(MediaCodec codec, CodecException e) {
|
|
||||||
if (e.isTransient()) {
|
|
||||||
LimeLog.warning(e.getDiagnosticInfo());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.severe(e.getDiagnosticInfo());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
LimeLog.info("Using hardware decoding");
|
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
videoDecoder.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() {
|
||||||
|
@Override
|
||||||
|
public void onFrameRendered(MediaCodec mediaCodec, long presentationTimeUs, long renderTimeNanos) {
|
||||||
|
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
if (USE_FRAME_RENDER_TIME) {
|
||||||
|
totalTimeMs += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Using codec "+selectedDecoderName+" for hardware decoding "+mimeType);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
|
||||||
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (e instanceof CodecException) {
|
if (e instanceof CodecException) {
|
||||||
CodecException codecExc = (CodecException) e;
|
CodecException codecExc = (CodecException) e;
|
||||||
@@ -170,23 +247,102 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only throw if this happens at the beginning of a stream
|
||||||
|
if (totalFrames < 60) {
|
||||||
if (buf != null || codecFlags != 0) {
|
if (buf != null || codecFlags != 0) {
|
||||||
throw new RendererException(dr, e, buf, codecFlags);
|
throw new RendererException(this, e, buf, codecFlags);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new RendererException(dr, e);
|
throw new RendererException(this, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRendererThread()
|
private void startDirectSubmitRendererThread()
|
||||||
|
{
|
||||||
|
rendererThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
BufferInfo info = new BufferInfo();
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
// Try to output a frame
|
||||||
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
||||||
|
if (outIndex >= 0) {
|
||||||
|
long presentationTimeUs = info.presentationTimeUs;
|
||||||
|
int lastIndex = outIndex;
|
||||||
|
|
||||||
|
// Get the last output buffer in the queue
|
||||||
|
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||||
|
|
||||||
|
lastIndex = outIndex;
|
||||||
|
presentationTimeUs = info.presentationTimeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the last buffer
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
|
||||||
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
|
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
decoderTimeMs += delta;
|
||||||
|
if (!USE_FRAME_RENDER_TIME) {
|
||||||
|
totalTimeMs += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (outIndex) {
|
||||||
|
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||||
|
break;
|
||||||
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
|
LimeLog.info("Output format changed");
|
||||||
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||||
|
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
|
||||||
|
rendererThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
|
||||||
|
int index;
|
||||||
|
long startTime, queueTime;
|
||||||
|
|
||||||
|
startTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
|
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
|
||||||
|
if (index < 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
|
||||||
|
if (queueTime - startTime >= 20) {
|
||||||
|
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startLegacyRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
DecodeUnit du = null;
|
DecodeUnit du = null;
|
||||||
int inputIndex = -1;
|
int inputIndex = -1;
|
||||||
|
long lastDuDequeueTime = 0;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
// In order to get as much data to the decoder as early as possible,
|
// In order to get as much data to the decoder as early as possible,
|
||||||
@@ -194,22 +350,26 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
if (inputIndex == -1 && du == null) {
|
if (inputIndex == -1 && du == null) {
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
inputIndex = dequeueInputBuffer(false, false);
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
notifyDuReceived(du);
|
||||||
|
}
|
||||||
|
|
||||||
// Stop if we can't get a DU or input buffer
|
// Stop if we can't get a DU or input buffer
|
||||||
if (du == null || inputIndex == -1) {
|
if (du == null || inputIndex == -1) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, inputIndex);
|
||||||
|
|
||||||
du = null;
|
du = null;
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,22 +384,31 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// If we've got a DU waiting to be given to the decoder,
|
// If we've got a DU waiting to be given to the decoder,
|
||||||
// wait a full 3 ms for an input buffer. Otherwise
|
// wait a full 3 ms for an input buffer. Otherwise
|
||||||
// just see if we can get one immediately.
|
// just see if we can get one immediately.
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
inputIndex = dequeueInputBuffer(du != null, false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a decode unit if we don't have one already
|
// Grab a decode unit if we don't have one already
|
||||||
if (du == null) {
|
if (du == null) {
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
notifyDuReceived(du);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've got both a decode unit and an input buffer, we'll
|
// If we've got both a decode unit and an input buffer, we'll
|
||||||
// submit now. Otherwise, we wait until we have one.
|
// submit now. Otherwise, we wait until we have one.
|
||||||
if (du != null && inputIndex >= 0) {
|
if (du != null && inputIndex >= 0) {
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
long submissionTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
if (submissionTime - lastDuDequeueTime >= 20) {
|
||||||
|
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
submitDecodeUnit(du, inputIndex);
|
||||||
|
|
||||||
// DU and input buffer have both been consumed
|
// DU and input buffer have both been consumed
|
||||||
du = null;
|
du = null;
|
||||||
@@ -265,11 +434,13 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
|
long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000);
|
||||||
if (delta > 5 && delta < 300) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
decoderTimeMs += delta;
|
decoderTimeMs += delta;
|
||||||
|
if (!USE_FRAME_RENDER_TIME) {
|
||||||
totalTimeMs += delta;
|
totalTimeMs += delta;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (outIndex) {
|
switch (outIndex) {
|
||||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||||
@@ -279,9 +450,6 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
LockSupport.parkNanos(1);
|
LockSupport.parkNanos(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
|
||||||
LimeLog.info("Output buffers changed");
|
|
||||||
break;
|
|
||||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
LimeLog.info("Output format changed");
|
LimeLog.info("Output format changed");
|
||||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
@@ -291,7 +459,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,11 +477,17 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
|
|
||||||
// On devices pre-Lollipop, we'll use a rendering thread
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
legacyInputBuffers = videoDecoder.getInputBuffers();
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
|
||||||
startRendererThread();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (directSubmit) {
|
||||||
|
startDirectSubmitRendererThread();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
startLegacyRendererThread();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +501,8 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException ignored) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the decoder
|
// We could stop the decoder here, but it seems to cause some problems
|
||||||
videoDecoder.stop();
|
// so we'll just let release take care of it.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -346,11 +520,11 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
for (i = 0; i < 25; i++) {
|
for (i = 0; i < 25; i++) {
|
||||||
try {
|
try {
|
||||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||||
0, length,
|
offset, length,
|
||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(this, e, null, codecFlags);
|
handleDecoderException(e, null, codecFlags);
|
||||||
lastException = e;
|
lastException = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,16 +534,44 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
// Using the new getInputBuffer() API on Lollipop allows
|
||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
// the framework to do some performance optimizations for us
|
||||||
long currentTime = System.currentTimeMillis();
|
private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) {
|
||||||
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
ByteBuffer buf;
|
||||||
if (delta >= 0 && delta < 300) {
|
|
||||||
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
totalFrames++;
|
buf = videoDecoder.getInputBuffer(inputBufferIndex);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buf = legacyInputBuffers[inputBufferIndex];
|
||||||
|
|
||||||
|
// Clear old input data pre-Lollipop
|
||||||
|
buf.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
long timestampUs = currentTime * 1000;
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doProfileSpecificSpsPatching(SeqParameterSet sps) {
|
||||||
|
// Some devices benefit from setting constraint flags 4 & 5 to make this Constrained
|
||||||
|
// High Profile which allows the decoder to assume there will be no B-frames and
|
||||||
|
// reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't
|
||||||
|
// like it so we only set them on devices that are confirmed to benefit from it.
|
||||||
|
if (sps.profile_idc == 100 && constrainedHighProfile) {
|
||||||
|
LimeLog.info("Setting constraint set flags for constrained high profile");
|
||||||
|
sps.constraint_set_4_flag = true;
|
||||||
|
sps.constraint_set_5_flag = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Force the constraints unset otherwise (some may be set by default)
|
||||||
|
sps.constraint_set_4_flag = false;
|
||||||
|
sps.constraint_set_5_flag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) {
|
||||||
|
long timestampUs = System.nanoTime() / 1000;
|
||||||
if (timestampUs <= lastTimestampUs) {
|
if (timestampUs <= lastTimestampUs) {
|
||||||
// We can't submit multiple buffers with the same timestamp
|
// We can't submit multiple buffers with the same timestamp
|
||||||
// so bump it up by one before queuing
|
// so bump it up by one before queuing
|
||||||
@@ -377,8 +579,7 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
lastTimestampUs = timestampUs;
|
lastTimestampUs = timestampUs;
|
||||||
|
|
||||||
// Clear old input data
|
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
|
||||||
buf.clear();
|
|
||||||
|
|
||||||
int codecFlags = 0;
|
int codecFlags = 0;
|
||||||
int decodeUnitFlags = decodeUnit.getFlags();
|
int decodeUnitFlags = decodeUnit.getFlags();
|
||||||
@@ -390,8 +591,12 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
numIframeIn++;
|
numIframeIn++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
ByteBufferDescriptor header = decodeUnit.getBufferHead();
|
||||||
|
|
||||||
|
// H264 SPS
|
||||||
if (header.data[header.offset+4] == 0x67) {
|
if (header.data[header.offset+4] == 0x67) {
|
||||||
numSpsIn++;
|
numSpsIn++;
|
||||||
|
|
||||||
@@ -400,7 +605,26 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
// Skip to the start of the NALU data
|
// Skip to the start of the NALU data
|
||||||
spsBuf.position(header.offset+5);
|
spsBuf.position(header.offset+5);
|
||||||
|
|
||||||
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
|
// The H264Utils.readSPS function safely handles
|
||||||
|
// Annex B NALUs (including NALUs with escape sequences)
|
||||||
|
SeqParameterSet sps = H264Utils.readSPS(spsBuf);
|
||||||
|
|
||||||
|
// Some decoders rely on H264 level to decide how many buffers are needed
|
||||||
|
// Since we only need one frame buffered, we'll set the level as low as we can
|
||||||
|
// for known resolution combinations
|
||||||
|
if (initialWidth == 1280 && initialHeight == 720) {
|
||||||
|
// Max 5 buffered frames at 1280x720x60
|
||||||
|
LimeLog.info("Patching level_idc to 32");
|
||||||
|
sps.level_idc = 32;
|
||||||
|
}
|
||||||
|
else if (initialWidth == 1920 && initialHeight == 1080) {
|
||||||
|
// Max 4 buffered frames at 1920x1080x64
|
||||||
|
LimeLog.info("Patching level_idc to 42");
|
||||||
|
sps.level_idc = 42;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Leave the profile alone (currently 5.0)
|
||||||
|
}
|
||||||
|
|
||||||
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
|
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
|
||||||
// also requires this fixup.
|
// also requires this fixup.
|
||||||
@@ -411,26 +635,63 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
LimeLog.info("Patching num_ref_frames in SPS");
|
LimeLog.info("Patching num_ref_frames in SPS");
|
||||||
sps.num_ref_frames = 1;
|
sps.num_ref_frames = 1;
|
||||||
|
|
||||||
|
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||||
|
// Some devices don't like these so we remove them here.
|
||||||
|
sps.vuiParams.video_signal_type_present_flag = false;
|
||||||
|
sps.vuiParams.colour_description_present_flag = false;
|
||||||
|
sps.vuiParams.chroma_loc_info_present_flag = false;
|
||||||
|
|
||||||
if (needsSpsBitstreamFixup || isExynos4) {
|
if (needsSpsBitstreamFixup || isExynos4) {
|
||||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||||
LimeLog.info("Adding bitstream restrictions");
|
|
||||||
|
|
||||||
|
// GFE 2.5.11 started sending bitstream restrictions
|
||||||
|
if (sps.vuiParams.bitstreamRestriction == null) {
|
||||||
|
LimeLog.info("Adding bitstream restrictions");
|
||||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||||
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
||||||
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
|
||||||
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
||||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
||||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
||||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Patching bitstream restrictions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
|
||||||
|
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
|
||||||
|
|
||||||
|
// These values are the defaults for the fields, but they are more aggressive
|
||||||
|
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
|
||||||
|
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
||||||
|
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
||||||
|
|
||||||
|
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
|
||||||
|
// conservative values by GFE 2.5.11. We'll let those values stand.
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
|
||||||
|
// will continue to not receive them now
|
||||||
|
sps.vuiParams.bitstreamRestriction = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to hack this SPS to say we're baseline, do so now
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Hacking SPS to baseline");
|
||||||
|
sps.profile_idc = 66;
|
||||||
|
savedSps = sps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch the SPS constraint flags
|
||||||
|
doProfileSpecificSpsPatching(sps);
|
||||||
|
|
||||||
// Write the annex B header
|
// Write the annex B header
|
||||||
buf.put(header.data, header.offset, 5);
|
buf.put(header.data, header.offset, 5);
|
||||||
|
|
||||||
// Write the modified SPS to the input buffer
|
// The H264Utils.writeSPS function safely handles
|
||||||
sps.write(buf);
|
// Annex B NALUs (including NALUs with escape sequences)
|
||||||
|
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, header.length);
|
||||||
|
buf.put(escapedNalu);
|
||||||
|
|
||||||
queueInputBuffer(inputBufferIndex,
|
queueInputBuffer(inputBufferIndex,
|
||||||
0, buf.position(),
|
0, buf.position(),
|
||||||
@@ -438,14 +699,33 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
|
|
||||||
depacketizer.freeDecodeUnit(decodeUnit);
|
depacketizer.freeDecodeUnit(decodeUnit);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// H264 PPS
|
||||||
} else if (header.data[header.offset+4] == 0x68) {
|
} else if (header.data[header.offset+4] == 0x68) {
|
||||||
numPpsIn++;
|
numPpsIn++;
|
||||||
|
|
||||||
|
if (needsBaselineSpsHack) {
|
||||||
|
LimeLog.info("Saw PPS; disabling SPS hack");
|
||||||
|
needsBaselineSpsHack = false;
|
||||||
|
|
||||||
|
// Give the decoder the SPS again with the proper profile now
|
||||||
|
needsSpsReplay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (header.data[header.offset+4] == 0x40) {
|
||||||
|
numVpsIn++;
|
||||||
|
}
|
||||||
|
else if (header.data[header.offset+4] == 0x42) {
|
||||||
|
numSpsIn++;
|
||||||
|
}
|
||||||
|
else if (header.data[header.offset+4] == 0x44) {
|
||||||
|
numPpsIn++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy data from our buffer list into the input buffer
|
// Copy data from our buffer list into the input buffer
|
||||||
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
|
||||||
{
|
desc != null; desc = desc.nextDescriptor) {
|
||||||
buf.put(desc.data, desc.offset, desc.length);
|
buf.put(desc.data, desc.offset, desc.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,12 +734,53 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
|
|
||||||
depacketizer.freeDecodeUnit(decodeUnit);
|
depacketizer.freeDecodeUnit(decodeUnit);
|
||||||
|
|
||||||
|
if (needsSpsReplay) {
|
||||||
|
replaySps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaySps() {
|
||||||
|
int inputIndex = dequeueInputBuffer(true, true);
|
||||||
|
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
|
||||||
|
|
||||||
|
// Write the Annex B header
|
||||||
|
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
|
||||||
|
|
||||||
|
// Switch the H264 profile back to high
|
||||||
|
savedSps.profile_idc = 100;
|
||||||
|
|
||||||
|
// Patch the SPS constraint flags
|
||||||
|
doProfileSpecificSpsPatching(savedSps);
|
||||||
|
|
||||||
|
// The H264Utils.writeSPS function safely handles
|
||||||
|
// Annex B NALUs (including NALUs with escape sequences)
|
||||||
|
ByteBuffer escapedNalu = H264Utils.writeSPS(savedSps, 128);
|
||||||
|
inputBuffer.put(escapedNalu);
|
||||||
|
|
||||||
|
// No need for the SPS anymore
|
||||||
|
savedSps = null;
|
||||||
|
|
||||||
|
// Queue the new SPS
|
||||||
|
queueInputBuffer(inputIndex,
|
||||||
|
0, inputBuffer.position(),
|
||||||
|
System.nanoTime() / 1000,
|
||||||
|
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
|
||||||
|
|
||||||
|
LimeLog.info("SPS replay complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return adaptivePlayback ?
|
int caps = 0;
|
||||||
|
|
||||||
|
caps |= adaptivePlayback ?
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
||||||
|
|
||||||
|
caps |= directSubmit ?
|
||||||
|
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
||||||
|
|
||||||
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -478,11 +799,40 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
return (int)(totalTimeMs / totalFrames);
|
return (int)(totalTimeMs / totalFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyDuReceived(DecodeUnit du) {
|
||||||
|
long currentTime = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
long delta = currentTime-du.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
totalTimeMs += currentTime-du.getReceiveTimestamp();
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void directSubmitDecodeUnit(DecodeUnit du) {
|
||||||
|
int inputIndex = -1;
|
||||||
|
|
||||||
|
notifyDuReceived(du);
|
||||||
|
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
try {
|
||||||
|
inputIndex = dequeueInputBuffer(true, true);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputIndex >= 0) {
|
||||||
|
submitDecodeUnit(du, inputIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class RendererException extends RuntimeException {
|
public class RendererException extends RuntimeException {
|
||||||
private static final long serialVersionUID = 8985937536997012406L;
|
private static final long serialVersionUID = 8985937536997012406L;
|
||||||
|
|
||||||
private Exception originalException;
|
private final Exception originalException;
|
||||||
private MediaCodecDecoderRenderer renderer;
|
private final MediaCodecDecoderRenderer renderer;
|
||||||
private ByteBuffer currentBuffer;
|
private ByteBuffer currentBuffer;
|
||||||
private int currentCodecFlags;
|
private int currentCodecFlags;
|
||||||
|
|
||||||
@@ -501,10 +851,14 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
String str = "";
|
String str = "";
|
||||||
|
|
||||||
str += "Decoder: "+renderer.decoderName+"\n";
|
str += "Format: "+renderer.videoFormat+"\n";
|
||||||
|
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
|
||||||
|
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||||
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||||
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
|
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
|
||||||
str += "Total frames: "+renderer.totalFrames+"\n";
|
str += "Total frames: "+renderer.totalFrames+"\n";
|
||||||
|
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
|
||||||
|
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
|
||||||
|
|
||||||
if (currentBuffer != null) {
|
if (currentBuffer != null) {
|
||||||
str += "Current buffer: ";
|
str += "Current buffer: ";
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ConfigurationInfo;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaCodecList;
|
import android.media.MediaCodecList;
|
||||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||||
@@ -20,35 +23,109 @@ import com.limelight.LimeLog;
|
|||||||
|
|
||||||
public class MediaCodecHelper {
|
public class MediaCodecHelper {
|
||||||
|
|
||||||
public static final List<String> preferredDecoders;
|
private static final List<String> preferredDecoders;
|
||||||
|
|
||||||
public static final List<String> blacklistedDecoderPrefixes;
|
private static final List<String> blacklistedDecoderPrefixes;
|
||||||
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
|
private static final List<String> baselineProfileHackPrefixes;
|
||||||
|
private static final List<String> directSubmitPrefixes;
|
||||||
|
private static final List<String> constrainedHighProfilePrefixes;
|
||||||
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
directSubmitPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
|
// These decoders have low enough input buffer latency that they
|
||||||
|
// can be directly invoked from the receive thread
|
||||||
|
directSubmitPrefixes.add("omx.qcom");
|
||||||
|
directSubmitPrefixes.add("omx.sec");
|
||||||
|
directSubmitPrefixes.add("omx.exynos");
|
||||||
|
directSubmitPrefixes.add("omx.intel");
|
||||||
|
directSubmitPrefixes.add("omx.brcm");
|
||||||
|
directSubmitPrefixes.add("omx.TI");
|
||||||
|
directSubmitPrefixes.add("omx.arc");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
preferredDecoders = new LinkedList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// Software decoders that don't support H264 high profile
|
// Software decoders that don't support H264 high profile
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
|
|
||||||
|
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||||
|
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||||
|
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
baselineProfileHackPrefixes = new LinkedList<>();
|
||||||
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
|
|
||||||
|
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||||
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
whitelistedHevcDecoders = new LinkedList<>();
|
||||||
|
|
||||||
|
// Exynos seems to be the only HEVC decoder that works reliably
|
||||||
|
whitelistedHevcDecoders.add("omx.exynos");
|
||||||
|
|
||||||
|
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
||||||
|
//whitelistedHevcDecoders.add("omx.nvidia");
|
||||||
|
|
||||||
|
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||||
|
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
||||||
|
// to actually be tested. Ugh...
|
||||||
|
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||||
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
}
|
||||||
|
|
||||||
|
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||||
|
// their AVC decoders), but haven't been tested enough
|
||||||
|
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
|
//whitelistedHevcDecoders.add("omx.rk");
|
||||||
|
|
||||||
|
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
||||||
|
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initializeWithContext(Context context) {
|
||||||
|
ActivityManager activityManager =
|
||||||
|
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
||||||
|
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
||||||
|
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
||||||
|
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
||||||
|
// 3xx - bad
|
||||||
|
// 4xx - good
|
||||||
|
//
|
||||||
|
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
||||||
|
// approximation by checking the GLES level (<= 3.0 is bad).
|
||||||
|
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||||
|
if (configInfo.reqGlEsVersion > 0x30000) {
|
||||||
|
LimeLog.info("Added omx.qcom to supported decoders based on GLES 3.1+ support");
|
||||||
|
whitelistedHevcDecoders.add("omx.qcom");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
@@ -64,8 +141,16 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long getMonotonicMillis() {
|
||||||
|
return System.nanoTime() / 1000000L;
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
|
||||||
|
/*
|
||||||
|
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||||
|
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||||
|
|
||||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
return true;
|
return true;
|
||||||
@@ -84,19 +169,56 @@ public class MediaCodecHelper {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Tolerate buggy codecs
|
// Tolerate buggy codecs
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
||||||
|
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderCanDirectSubmit(String decoderName) {
|
||||||
|
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
|
||||||
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
||||||
|
// TODO: Shield Tablet K1/LTE?
|
||||||
|
//
|
||||||
|
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||||
|
// whether the performance is good enough to use for streaming, but they're
|
||||||
|
// using the same omx.nvidia.h265.decode name as the Shield TV which has a
|
||||||
|
// fully accelerated HEVC pipeline. AFAIK, the only K1 device with this
|
||||||
|
// partially accelerated HEVC decoder is the Shield Tablet, so I'll
|
||||||
|
// check for it here.
|
||||||
|
//
|
||||||
|
// TODO: Temporarily disabled with NVIDIA HEVC support
|
||||||
|
/*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) {
|
||||||
|
return false;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
|
||||||
|
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
|
||||||
|
// so I'm restricting HEVC usage to Lollipop and higher.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
@@ -133,7 +255,7 @@ public class MediaCodecHelper {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaCodecInfo findPreferredDecoder() {
|
private static MediaCodecInfo findPreferredDecoder() {
|
||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
// because we want to evaluate the decoders in our list's order
|
// because we want to evaluate the decoders in our list's order
|
||||||
// rather than MediaCodecList's order
|
// rather than MediaCodecList's order
|
||||||
@@ -156,7 +278,7 @@ public class MediaCodecHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaCodecInfo findFirstDecoder() {
|
public static MediaCodecInfo findFirstDecoder(String mimeType) {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
@@ -169,9 +291,9 @@ public class MediaCodecHelper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a decoder that supports H.264
|
// Find a decoder that supports the specified video format
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
if (mime.equalsIgnoreCase("video/avc")) {
|
if (mime.equalsIgnoreCase(mimeType)) {
|
||||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
||||||
return codecInfo;
|
return codecInfo;
|
||||||
}
|
}
|
||||||
@@ -181,7 +303,7 @@ public class MediaCodecHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaCodecInfo findProbableSafeDecoder() {
|
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
|
||||||
// First look for a preferred decoder by name
|
// First look for a preferred decoder by name
|
||||||
MediaCodecInfo info = findPreferredDecoder();
|
MediaCodecInfo info = findPreferredDecoder();
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
@@ -191,12 +313,12 @@ public class MediaCodecHelper {
|
|||||||
// Now look for decoders we know are safe
|
// Now look for decoders we know are safe
|
||||||
try {
|
try {
|
||||||
// If this function completes, it will determine if the decoder is safe
|
// If this function completes, it will determine if the decoder is safe
|
||||||
return findKnownSafeDecoder();
|
return findKnownSafeDecoder(mimeType, requiredProfile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Some buggy devices seem to throw exceptions
|
// Some buggy devices seem to throw exceptions
|
||||||
// from getCapabilitiesForType() so we'll just assume
|
// from getCapabilitiesForType() so we'll just assume
|
||||||
// they're okay and go with the first one we find
|
// they're okay and go with the first one we find
|
||||||
return findFirstDecoder();
|
return findFirstDecoder(mimeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +326,7 @@ public class MediaCodecHelper {
|
|||||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||||
// and we want to be sure all callers are handling this possibility
|
// and we want to be sure all callers are handling this possibility
|
||||||
@SuppressWarnings("RedundantThrows")
|
@SuppressWarnings("RedundantThrows")
|
||||||
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
@@ -217,21 +339,26 @@ public class MediaCodecHelper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a decoder that supports H.264 high profile
|
// Find a decoder that supports the requested video format
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
if (mime.equalsIgnoreCase("video/avc")) {
|
if (mime.equalsIgnoreCase(mimeType)) {
|
||||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
||||||
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||||
|
|
||||||
|
if (requiredProfile != -1) {
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||||
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
|
if (profile.profile == requiredProfile) {
|
||||||
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
|
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
|
||||||
LimeLog.info("Selected decoder: "+codecInfo.getName());
|
|
||||||
return codecInfo;
|
return codecInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return codecInfo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class ComputerDatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
public void deleteComputer(String name) {
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
@@ -68,7 +68,7 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
public List<ComputerDetails> getAllComputers() {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
@@ -100,6 +100,7 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
// If a field is corrupt or missing, skip the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
@@ -107,6 +108,7 @@ public class ComputerDatabaseManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
computerList.add(details);
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ public class ComputerDatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByName(String name) {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
@@ -152,6 +154,9 @@ public class ComputerDatabaseManager {
|
|||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
// If a field is corrupt or missing, delete the database entry
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
details.macAddress == null) {
|
details.macAddress == null) {
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.HashMap;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Timer;
|
import java.util.UUID;
|
||||||
import java.util.TimerTask;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.discovery.DiscoveryService;
|
import com.limelight.discovery.DiscoveryService;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -25,19 +28,29 @@ import android.content.ServiceConnection;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
public class ComputerManagerService extends Service {
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
private static final int POLLING_PERIOD_MS = 5000;
|
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
|
||||||
|
|
||||||
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
public class ComputerManagerService extends Service {
|
||||||
|
private static final int SERVERINFO_POLLING_PERIOD_MS = 1500;
|
||||||
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
private static final int FAST_POLL_TIMEOUT = 500;
|
||||||
|
private static final int OFFLINE_POLL_TRIES = 5;
|
||||||
|
private static final int INITIAL_POLL_TRIES = 2;
|
||||||
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
|
private static final int POLL_DATA_TTL_MS = 30000;
|
||||||
|
|
||||||
|
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
private ComputerDatabaseManager dbManager;
|
private ComputerDatabaseManager dbManager;
|
||||||
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
private IdentityManager idManager;
|
private IdentityManager idManager;
|
||||||
private HashMap<ComputerDetails, Thread> pollingThreads;
|
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
|
||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
private AtomicInteger activePolls = new AtomicInteger(0);
|
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
|
private boolean pollingActive = false;
|
||||||
|
|
||||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||||
@@ -60,23 +73,34 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Returns true if the details object was modified
|
// Returns true if the details object was modified
|
||||||
private boolean runPoll(ComputerDetails details)
|
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
|
||||||
{
|
|
||||||
boolean newPc = (details.name == null);
|
|
||||||
|
|
||||||
if (!getLocalDatabaseReference()) {
|
if (!getLocalDatabaseReference()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final int pollTriesBeforeOffline = details.state == ComputerDetails.State.UNKNOWN ?
|
||||||
|
INITIAL_POLL_TRIES : OFFLINE_POLL_TRIES;
|
||||||
|
|
||||||
activePolls.incrementAndGet();
|
activePolls.incrementAndGet();
|
||||||
|
|
||||||
// Poll the machine
|
// Poll the machine
|
||||||
if (!doPollMachine(details)) {
|
try {
|
||||||
|
if (!pollComputer(details)) {
|
||||||
|
if (!newPc && offlineCount < pollTriesBeforeOffline) {
|
||||||
|
// Return without calling the listener
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
activePolls.decrementAndGet();
|
activePolls.decrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
// If it's online, update our persistent state
|
// If it's online, update our persistent state
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
@@ -86,7 +110,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (dbManager.getComputerByName(details.name) == null) {
|
if (dbManager.getComputerByName(details.name) == null) {
|
||||||
// It's gone
|
// It's gone
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,58 +126,65 @@ public class ComputerManagerService extends Service {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Thread createPollingThread(final ComputerDetails details) {
|
private Thread createPollingThread(final PollingTuple tuple) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
while (!isInterrupted()) {
|
|
||||||
ComputerDetails originalDetails = new ComputerDetails();
|
|
||||||
originalDetails.update(details);
|
|
||||||
|
|
||||||
|
int offlineCount = 0;
|
||||||
|
while (!isInterrupted() && pollingActive) {
|
||||||
|
try {
|
||||||
|
// Only allow one request to the machine at a time
|
||||||
|
synchronized (tuple.networkLock) {
|
||||||
// Check if this poll has modified the details
|
// Check if this poll has modified the details
|
||||||
if (runPoll(details) && !originalDetails.equals(details)) {
|
if (!runPoll(tuple.computer, false, offlineCount)) {
|
||||||
// Replace our thread entry with the new one
|
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
||||||
synchronized (pollingThreads) {
|
offlineCount++;
|
||||||
pollingThreads.remove(originalDetails);
|
} else {
|
||||||
pollingThreads.put(details, this);
|
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
|
||||||
|
offlineCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until the next polling interval
|
// Wait until the next polling interval
|
||||||
try {
|
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS);
|
||||||
Thread.sleep(POLLING_PERIOD_MS);
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for "+details.localIp.getHostAddress());
|
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ComputerManagerBinder extends Binder {
|
public class ComputerManagerBinder extends Binder {
|
||||||
public void startPolling(ComputerManagerListener listener) {
|
public void startPolling(ComputerManagerListener listener) {
|
||||||
|
// Polling is active
|
||||||
|
pollingActive = true;
|
||||||
|
|
||||||
// Set the listener
|
// Set the listener
|
||||||
ComputerManagerService.this.listener = listener;
|
ComputerManagerService.this.listener = listener;
|
||||||
|
|
||||||
// Start mDNS autodiscovery too
|
// Start mDNS autodiscovery too
|
||||||
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||||
|
|
||||||
// Start polling known machines
|
synchronized (pollingTuples) {
|
||||||
if (!getLocalDatabaseReference()) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
return;
|
// Enforce the poll data TTL
|
||||||
|
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||||
|
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||||
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
}
|
}
|
||||||
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
|
||||||
releaseLocalDatabaseReference();
|
|
||||||
|
|
||||||
synchronized (pollingThreads) {
|
// Report this computer initially
|
||||||
for (ComputerDetails computer : computerList) {
|
listener.notifyComputerUpdated(tuple.computer);
|
||||||
|
|
||||||
// This polling thread might already be there
|
// This polling thread might already be there
|
||||||
if (!pollingThreads.containsKey(computer)) {
|
if (tuple.thread == null) {
|
||||||
Thread t = createPollingThread(computer);
|
tuple.thread = createPollingThread(tuple);
|
||||||
pollingThreads.put(computer, t);
|
tuple.thread.start();
|
||||||
t.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,10 +214,6 @@ public class ComputerManagerService extends Service {
|
|||||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addComputer(InetAddress addr) {
|
|
||||||
ComputerManagerService.this.addComputer(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
ComputerManagerService.this.removeComputer(name);
|
ComputerManagerService.this.removeComputer(name);
|
||||||
}
|
}
|
||||||
@@ -196,22 +223,59 @@ public class ComputerManagerService extends Service {
|
|||||||
ComputerManagerService.this.onUnbind(null);
|
ComputerManagerService.this.onUnbind(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ApplistPoller createAppListPoller(ComputerDetails computer) {
|
||||||
|
return new ApplistPoller(computer);
|
||||||
|
}
|
||||||
|
|
||||||
public String getUniqueId() {
|
public String getUniqueId() {
|
||||||
return idManager.getUniqueId();
|
return idManager.getUniqueId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ComputerDetails getComputer(UUID uuid) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
|
return tuple.computer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invalidateStateForComputer(UUID uuid) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
|
// We need the network lock to prevent a concurrent poll
|
||||||
|
// from wiping this change out
|
||||||
|
synchronized (tuple.networkLock) {
|
||||||
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onUnbind(Intent intent) {
|
public boolean onUnbind(Intent intent) {
|
||||||
|
if (discoveryBinder != null) {
|
||||||
// Stop mDNS autodiscovery
|
// Stop mDNS autodiscovery
|
||||||
discoveryBinder.stopDiscovery();
|
discoveryBinder.stopDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
synchronized (pollingThreads) {
|
pollingActive = false;
|
||||||
for (Thread t : pollingThreads.values()) {
|
synchronized (pollingTuples) {
|
||||||
t.interrupt();
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
// Interrupt and remove the thread
|
||||||
|
tuple.thread.interrupt();
|
||||||
|
tuple.thread = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pollingThreads.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the listener
|
// Remove the listener
|
||||||
@@ -225,7 +289,7 @@ public class ComputerManagerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputer(computer.getAddress());
|
addComputerBlocking(computer.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -241,19 +305,35 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addComputer(InetAddress addr) {
|
private void addTuple(ComputerDetails details) {
|
||||||
// Setup a placeholder
|
synchronized (pollingTuples) {
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
fakeDetails.localIp = addr;
|
// Check if this is the same computer
|
||||||
fakeDetails.remoteIp = addr;
|
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||||
|
// Update details anyway in case this machine has been re-added by IP
|
||||||
|
// after not being reachable by our existing information
|
||||||
|
tuple.computer.localIp = details.localIp;
|
||||||
|
tuple.computer.remoteIp = details.remoteIp;
|
||||||
|
|
||||||
// Spawn a thread for this computer
|
// Start a polling thread if polling is active
|
||||||
synchronized (pollingThreads) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
// This polling thread might already be there
|
tuple.thread = createPollingThread(tuple);
|
||||||
if (!pollingThreads.containsKey(fakeDetails)) {
|
tuple.thread.start();
|
||||||
Thread t = createPollingThread(fakeDetails);
|
}
|
||||||
pollingThreads.put(fakeDetails, t);
|
|
||||||
t.start();
|
// Found an entry so we're done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, we didn't find an entry
|
||||||
|
PollingTuple tuple = new PollingTuple(details, null);
|
||||||
|
if (pollingActive) {
|
||||||
|
tuple.thread = createPollingThread(tuple);
|
||||||
|
}
|
||||||
|
pollingTuples.add(tuple);
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
tuple.thread.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,10 +345,23 @@ public class ComputerManagerService extends Service {
|
|||||||
fakeDetails.remoteIp = addr;
|
fakeDetails.remoteIp = addr;
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
runPoll(fakeDetails);
|
try {
|
||||||
|
runPoll(fakeDetails, true, 0);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If the machine is reachable, it was successful
|
// If the machine is reachable, it was successful
|
||||||
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
|
||||||
|
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||||
|
|
||||||
|
// Start a polling thread for this machine
|
||||||
|
addTuple(fakeDetails);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
@@ -279,6 +372,20 @@ public class ComputerManagerService extends Service {
|
|||||||
// Remove it from the database
|
// Remove it from the database
|
||||||
dbManager.deleteComputer(name);
|
dbManager.deleteComputer(name);
|
||||||
|
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
// Remove the computer from the computer list
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (tuple.computer.name.equals(name)) {
|
||||||
|
if (tuple.thread != null) {
|
||||||
|
// Interrupt the thread on this entry
|
||||||
|
tuple.thread.interrupt();
|
||||||
|
}
|
||||||
|
pollingTuples.remove(tuple);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,59 +404,196 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(InetAddress ipAddr) {
|
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
||||||
|
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||||
|
if (!fastPollIp(ipAddr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
return http.getComputerDetails();
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
|
|
||||||
|
// Check if this is the PC we expected
|
||||||
|
if (details.uuid != null && newDetails.uuid != null &&
|
||||||
|
!details.uuid.equals(newDetails.uuid)) {
|
||||||
|
// We got the wrong PC!
|
||||||
|
LimeLog.info("Polling returned the wrong PC!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDetails;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
ComputerDetails polledDetails;
|
// GFE server
|
||||||
|
private boolean fastPollIp(InetAddress addr) {
|
||||||
|
Socket s = new Socket();
|
||||||
|
try {
|
||||||
|
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
|
s.close();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (localFirst) {
|
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
||||||
polledDetails = tryPollIp(details.localIp);
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean pollRes = fastPollIp(addr);
|
||||||
|
|
||||||
|
synchronized (info) {
|
||||||
|
info[0] = true; // Done
|
||||||
|
info[1] = pollRes; // Polling result
|
||||||
|
|
||||||
|
info.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.setName("Fast Poll - "+addr.getHostAddress());
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
||||||
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
|
final boolean[] localInfo = new boolean[2];
|
||||||
|
|
||||||
|
startFastPollThread(local, localInfo);
|
||||||
|
startFastPollThread(remote, remoteInfo);
|
||||||
|
|
||||||
|
// Check local first
|
||||||
|
synchronized (localInfo) {
|
||||||
|
while (!localInfo[0]) {
|
||||||
|
localInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localInfo[1]) {
|
||||||
|
return ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now remote
|
||||||
|
synchronized (remoteInfo) {
|
||||||
|
while (!remoteInfo[0]) {
|
||||||
|
remoteInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteInfo[1]) {
|
||||||
|
return ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComputerDetails.Reachability.OFFLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||||
|
ComputerDetails polledDetails;
|
||||||
|
ComputerDetails.Reachability reachability;
|
||||||
|
|
||||||
|
// If the local address is routable across the Internet,
|
||||||
|
// always consider this PC remote to be conservative
|
||||||
|
if (details.localIp.equals(details.remoteIp)) {
|
||||||
|
reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details.remoteIp);
|
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||||
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||||
|
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||||
|
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||||
|
|
||||||
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
|
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
|
||||||
|
if (localFirst) {
|
||||||
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
InetAddress reachableAddr = null;
|
||||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||||
// Failed, so let's try the fallback
|
// Failed, so let's try the fallback
|
||||||
if (!localFirst) {
|
if (!localFirst) {
|
||||||
polledDetails = tryPollIp(details.localIp);
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
polledDetails = tryPollIp(details.remoteIp);
|
polledDetails = tryPollIp(details, details.remoteIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fallback poll worked
|
|
||||||
if (polledDetails != null) {
|
if (polledDetails != null) {
|
||||||
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
// The fallback poll worked
|
||||||
ComputerDetails.Reachability.REMOTE;
|
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (polledDetails != null) {
|
else if (polledDetails != null) {
|
||||||
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
||||||
ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Machine was unreachable both tries
|
if (reachableAddr == null) {
|
||||||
if (polledDetails == null) {
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
||||||
|
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
else if (polledDetails.localIp.equals(reachableAddr)) {
|
||||||
|
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReachabilityTuple(polledDetails, reachableAddr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
|
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
||||||
|
if (initialReachTuple == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we got here, it's reachable
|
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
details.update(polledDetails);
|
// Neither IP address reported in the serverinfo response was the one we used.
|
||||||
return true;
|
// Poll again to see if we can contact this machine on either of its reported addresses.
|
||||||
|
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||||
|
if (confirmationReachTuple == null) {
|
||||||
|
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||||
|
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
||||||
|
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We got it on one of the returned addresses; replace the original reach tuple
|
||||||
|
// with the new one
|
||||||
|
initialReachTuple = confirmationReachTuple;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doPollMachine(ComputerDetails details) {
|
// Save the old MAC address
|
||||||
return pollComputer(details, true);
|
String savedMacAddress = details.macAddress;
|
||||||
|
|
||||||
|
// If we got here, it's reachable
|
||||||
|
details.update(initialReachTuple.computer);
|
||||||
|
|
||||||
|
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||||
|
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||||
|
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
||||||
|
details.macAddress = savedMacAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -358,14 +602,24 @@ public class ComputerManagerService extends Service {
|
|||||||
bindService(new Intent(this, DiscoveryService.class),
|
bindService(new Intent(this, DiscoveryService.class),
|
||||||
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
pollingThreads = new HashMap<ComputerDetails, Thread>();
|
|
||||||
|
|
||||||
// Lookup or generate this device's UID
|
// Lookup or generate this device's UID
|
||||||
idManager = new IdentityManager(this);
|
idManager = new IdentityManager(this);
|
||||||
|
|
||||||
// Initialize the DB
|
// Initialize the DB
|
||||||
dbManager = new ComputerDatabaseManager(this);
|
dbManager = new ComputerDatabaseManager(this);
|
||||||
dbRefCount.set(1);
|
dbRefCount.set(1);
|
||||||
|
|
||||||
|
// Grab known machines into our computer list
|
||||||
|
if (!getLocalDatabaseReference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
|
// Add tuples for each computer
|
||||||
|
addTuple(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -385,4 +639,188 @@ public class ComputerManagerService extends Service {
|
|||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return binder;
|
return binder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ApplistPoller {
|
||||||
|
private Thread thread;
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final Object pollEvent = new Object();
|
||||||
|
private boolean receivedAppList = false;
|
||||||
|
|
||||||
|
public ApplistPoller(ComputerDetails computer) {
|
||||||
|
this.computer = computer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pollNow() {
|
||||||
|
synchronized (pollEvent) {
|
||||||
|
pollEvent.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean waitPollingDelay() {
|
||||||
|
try {
|
||||||
|
synchronized (pollEvent) {
|
||||||
|
if (receivedAppList) {
|
||||||
|
// If we've already reported an app list successfully,
|
||||||
|
// wait the full polling period
|
||||||
|
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we've failed to get an app list so far, retry much earlier
|
||||||
|
pollEvent.wait(APPLIST_FAILED_POLLING_RETRY_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return thread != null && !thread.isInterrupted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PollingTuple getPollingTuple(ComputerDetails details) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (details.uuid.equals(tuple.computer.uuid)) {
|
||||||
|
return tuple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
thread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int emptyAppListResponses = 0;
|
||||||
|
do {
|
||||||
|
InetAddress selectedAddr;
|
||||||
|
|
||||||
|
// Can't poll if it's not online
|
||||||
|
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.notifyComputerUpdated(computer);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't poll if there's no UUID yet
|
||||||
|
if (computer.uuid == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
selectedAddr = computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedAddr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
||||||
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
|
PollingTuple tuple = getPollingTuple(computer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String appList;
|
||||||
|
if (tuple != null) {
|
||||||
|
// If we're polling this machine too, grab the network lock
|
||||||
|
// while doing the app list request to prevent other requests
|
||||||
|
// from being issued in the meantime.
|
||||||
|
synchronized (tuple.networkLock) {
|
||||||
|
appList = http.getAppListRaw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No polling is happening now, so we just call it directly
|
||||||
|
appList = http.getAppListRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||||
|
|
||||||
|
// The app list might actually be empty, so if we get an empty response a few times
|
||||||
|
// in a row, we'll go ahead and believe it.
|
||||||
|
emptyAppListResponses++;
|
||||||
|
}
|
||||||
|
if (appList != null && !appList.isEmpty() &&
|
||||||
|
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||||
|
// Open the cache file
|
||||||
|
OutputStream cacheOut = null;
|
||||||
|
try {
|
||||||
|
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||||
|
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (cacheOut != null) {
|
||||||
|
cacheOut.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset empty count if it wasn't empty this time
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
emptyAppListResponses = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the computer
|
||||||
|
computer.rawAppList = appList;
|
||||||
|
receivedAppList = true;
|
||||||
|
|
||||||
|
// Notify that the app list has been updated
|
||||||
|
// and ensure that the thread is still active
|
||||||
|
if (listener != null && thread != null) {
|
||||||
|
listener.notifyComputerUpdated(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (appList == null || appList.isEmpty()) {
|
||||||
|
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} while (waitPollingDelay());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (thread != null) {
|
||||||
|
thread.interrupt();
|
||||||
|
|
||||||
|
// Don't join here because we might be blocked on network I/O
|
||||||
|
|
||||||
|
thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollingTuple {
|
||||||
|
public Thread thread;
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
public final Object networkLock;
|
||||||
|
public long lastSuccessfulPollMs;
|
||||||
|
|
||||||
|
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.thread = thread;
|
||||||
|
this.networkLock = new Object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReachabilityTuple {
|
||||||
|
public final InetAddress reachableAddress;
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
|
||||||
|
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.reachableAddress = reachableAddress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private DiscoveryBinder binder = new DiscoveryBinder();
|
private final DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.grid.assets.CachedAppAssetLoader;
|
||||||
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
|
import com.limelight.grid.assets.MemoryAssetLoader;
|
||||||
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
private static final int ART_WIDTH_PX = 300;
|
||||||
|
private static final int SMALL_WIDTH_DP = 100;
|
||||||
|
private static final int LARGE_WIDTH_DP = 150;
|
||||||
|
|
||||||
|
private final CachedAppAssetLoader loader;
|
||||||
|
|
||||||
|
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||||
|
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
||||||
|
|
||||||
|
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||||
|
int dp;
|
||||||
|
|
||||||
|
if (small) {
|
||||||
|
dp = SMALL_WIDTH_DP;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dp = LARGE_WIDTH_DP;
|
||||||
|
}
|
||||||
|
|
||||||
|
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
||||||
|
if (scalingDivisor < 1.0) {
|
||||||
|
// We don't want to make them bigger before draw-time
|
||||||
|
scalingDivisor = 1.0;
|
||||||
|
}
|
||||||
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inSampleSize = (int) scalingDivisor;
|
||||||
|
|
||||||
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
|
new MemoryAssetLoader(),
|
||||||
|
new DiskAssetLoader(context.getCacheDir()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelQueuedOperations() {
|
||||||
|
loader.cancelForegroundLoads();
|
||||||
|
loader.cancelBackgroundLoads();
|
||||||
|
loader.freeCacheMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortList() {
|
||||||
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
|
@Override
|
||||||
|
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||||
|
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addApp(AppView.AppObject app) {
|
||||||
|
// Queue a request to fetch this bitmap into cache
|
||||||
|
loader.queueCacheLoad(app.app);
|
||||||
|
|
||||||
|
// Add the app to our sorted list
|
||||||
|
itemList.add(app);
|
||||||
|
sortList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeApp(AppView.AppObject app) {
|
||||||
|
itemList.remove(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
||||||
|
// Let the cached asset loader handle it
|
||||||
|
loader.populateImageView(obj.app, imgView, prgView);
|
||||||
|
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.isRunning) {
|
||||||
|
// Show the play button overlay
|
||||||
|
overlayView.setImageResource(R.drawable.ic_play);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
|
protected final Context context;
|
||||||
|
protected final int layoutId;
|
||||||
|
protected final ArrayList<T> itemList = new ArrayList<>();
|
||||||
|
protected final LayoutInflater inflater;
|
||||||
|
|
||||||
|
public GenericGridAdapter(Context context, int layoutId) {
|
||||||
|
this.context = context;
|
||||||
|
this.layoutId = layoutId;
|
||||||
|
|
||||||
|
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, ProgressBar prgView, 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);
|
||||||
|
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
|
if (imgView != null) {
|
||||||
|
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||||
|
imgView.setImageBitmap(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!populateTextView(txtView, itemList.get(i))) {
|
||||||
|
txtView.setText(itemList.get(i).toString());
|
||||||
|
}
|
||||||
|
if (overlayView != null) {
|
||||||
|
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
||||||
|
overlayView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.PcView;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
|
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
||||||
|
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
|
itemList.add(computer);
|
||||||
|
sortList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortList() {
|
||||||
|
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
||||||
|
@Override
|
||||||
|
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
||||||
|
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeComputer(PcView.ComputerObject computer) {
|
||||||
|
return itemList.remove(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
imgView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
imgView.setAlpha(0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
|
prgView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
imgView.setImageResource(R.drawable.ic_computer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||||
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
|
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.state == ComputerDetails.State.OFFLINE) {
|
||||||
|
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
||||||
|
overlayView.setAlpha(0.4f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CachedAppAssetLoader {
|
||||||
|
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
||||||
|
|
||||||
|
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
||||||
|
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
||||||
|
private static final int MAX_PENDING_DISK_LOADS = 40;
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final double scalingDivider;
|
||||||
|
private final NetworkAssetLoader networkLoader;
|
||||||
|
private final MemoryAssetLoader memoryLoader;
|
||||||
|
private final DiskAssetLoader diskLoader;
|
||||||
|
private final Bitmap placeholderBitmap;
|
||||||
|
|
||||||
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
|
DiskAssetLoader diskLoader) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.scalingDivider = scalingDivider;
|
||||||
|
this.networkLoader = networkLoader;
|
||||||
|
this.memoryLoader = memoryLoader;
|
||||||
|
this.diskLoader = diskLoader;
|
||||||
|
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelBackgroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
||||||
|
cacheExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelForegroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
|
||||||
|
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
||||||
|
foregroundExecutor.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((r = networkExecutor.getQueue().poll()) != null) {
|
||||||
|
networkExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void freeCacheMemory() {
|
||||||
|
memoryLoader.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||||
|
// Try 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
// Check again whether we've been cancelled or the image view is gone
|
||||||
|
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||||
|
if (in != null) {
|
||||||
|
// Write the stream straight to disk
|
||||||
|
diskLoader.populateCacheWithStream(tuple, in);
|
||||||
|
|
||||||
|
// Close the network input stream
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
|
if (task != null) {
|
||||||
|
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp != null) {
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise it's a background load and we return nothing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second with a bit of fuzz
|
||||||
|
try {
|
||||||
|
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||||
|
private final WeakReference<ImageView> imageViewRef;
|
||||||
|
private final WeakReference<ProgressBar> progressViewRef;
|
||||||
|
private final boolean diskOnly;
|
||||||
|
|
||||||
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
|
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
||||||
|
this.imageViewRef = new WeakReference<>(imageView);
|
||||||
|
this.progressViewRef = new WeakReference<>(prgView);
|
||||||
|
this.diskOnly = diskOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||||
|
tuple = params[0];
|
||||||
|
|
||||||
|
// Check whether it has been cancelled or the views are gone
|
||||||
|
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp == null) {
|
||||||
|
if (!diskOnly) {
|
||||||
|
// Try to load the asset from the network
|
||||||
|
bmp = doNetworkAssetLoad(tuple, this);
|
||||||
|
} else {
|
||||||
|
// Report progress to display the placeholder and spin
|
||||||
|
// off the network-capable task
|
||||||
|
publishProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the bitmap
|
||||||
|
if (bmp != null) {
|
||||||
|
memoryLoader.populateCache(tuple, bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Void... nothing) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current loader task for this view isn't us, do nothing
|
||||||
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
final ProgressBar prgView = progressViewRef.get();
|
||||||
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Now display the progress bar since we have to hit the network
|
||||||
|
if (prgView != null) {
|
||||||
|
prgView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set off another loader task on the network executor
|
||||||
|
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||||
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
final ProgressBar prgView = progressViewRef.get();
|
||||||
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Set the bitmap
|
||||||
|
if (bitmap != null) {
|
||||||
|
imageView.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the progress bar
|
||||||
|
if (prgView != null) {
|
||||||
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the view
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class AsyncDrawable extends BitmapDrawable {
|
||||||
|
private final WeakReference<LoaderTask> loaderTaskReference;
|
||||||
|
|
||||||
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
|
LoaderTask loaderTask) {
|
||||||
|
super(res, bitmap);
|
||||||
|
loaderTaskReference = new WeakReference<>(loaderTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderTask getLoaderTask() {
|
||||||
|
return loaderTaskReference.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoaderTask getLoaderTask(ImageView imageView) {
|
||||||
|
if (imageView == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Drawable drawable = imageView.getDrawable();
|
||||||
|
|
||||||
|
// If our drawable is in play, get the loader task
|
||||||
|
if (drawable instanceof AsyncDrawable) {
|
||||||
|
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||||
|
return asyncDrawable.getLoaderTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
||||||
|
final LoaderTask loaderTask = getLoaderTask(imageView);
|
||||||
|
|
||||||
|
// Check if any task was pending for this image view
|
||||||
|
if (loaderTask != null && !loaderTask.isCancelled()) {
|
||||||
|
final LoaderTuple taskTuple = loaderTask.tuple;
|
||||||
|
|
||||||
|
// Cancel the task if it's not already loading the same data
|
||||||
|
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
||||||
|
loaderTask.cancel(true);
|
||||||
|
} else {
|
||||||
|
// It's already loading what we want
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the load to proceed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queueCacheLoad(NvApp app) {
|
||||||
|
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
||||||
|
// It's in memory which means it must also be on disk
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue a fetch in the cache executor
|
||||||
|
cacheExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Check if the image is cached on disk
|
||||||
|
if (diskLoader.checkCacheExists(tuple)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the asset from the network and cache result on disk
|
||||||
|
doNetworkAssetLoad(tuple, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
||||||
|
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
// If there's already a task in progress for this view,
|
||||||
|
// cancel it. If the task is already loading the same image,
|
||||||
|
// we return and let that load finish.
|
||||||
|
if (!cancelPendingLoad(tuple, imgView)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the progress bar always on initial load
|
||||||
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
// First, try the memory cache in the current context
|
||||||
|
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
|
if (bmp != null) {
|
||||||
|
// Show the bitmap immediately
|
||||||
|
imgView.setVisibility(View.VISIBLE);
|
||||||
|
imgView.setImageBitmap(bmp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not in memory, create an async task to load it. This task will be attached
|
||||||
|
// via AsyncDrawable to this view.
|
||||||
|
final LoaderTask task = new LoaderTask(imgView, prgView, true);
|
||||||
|
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||||
|
imgView.setVisibility(View.INVISIBLE);
|
||||||
|
imgView.setImageDrawable(asyncDrawable);
|
||||||
|
|
||||||
|
// Run the task on our foreground executor
|
||||||
|
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoaderTuple {
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
public final NvApp app;
|
||||||
|
|
||||||
|
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof LoaderTuple)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoaderTuple other = (LoaderTuple) o;
|
||||||
|
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class DiskAssetLoader {
|
||||||
|
// 5 MB
|
||||||
|
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
private final File cacheDir;
|
||||||
|
|
||||||
|
public DiskAssetLoader(File cacheDir) {
|
||||||
|
this.cacheDir = cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
|
InputStream in = null;
|
||||||
|
Bitmap bmp = null;
|
||||||
|
try {
|
||||||
|
// Make sure the cached asset doesn't exceed the maximum size
|
||||||
|
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
||||||
|
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||||
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inSampleSize = sampleSize;
|
||||||
|
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||||
|
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
|
OutputStream out = null;
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||||
|
success = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (out != null) {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||||
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.LruCache;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class MemoryAssetLoader {
|
||||||
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
|
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
||||||
|
@Override
|
||||||
|
protected int sizeOf(String key, Bitmap bitmap) {
|
||||||
|
// Sizeof returns kilobytes
|
||||||
|
return bitmap.getByteCount() / 1024;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||||
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCache() {
|
||||||
|
memoryCache.evictAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
public class NetworkAssetLoader {
|
||||||
|
private final Context context;
|
||||||
|
private final String uniqueId;
|
||||||
|
|
||||||
|
public NetworkAssetLoader(Context context, String uniqueId) {
|
||||||
|
this.context = context;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||||
|
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = http.getBoxArt(tuple.app);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
if (in != null) {
|
||||||
|
LimeLog.info("Network asset load complete: " + tuple);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Network asset load failed: " + tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.limelight.nvstream.av.video.cpu;
|
|
||||||
|
|
||||||
public class AvcDecoder {
|
|
||||||
static {
|
|
||||||
// FFMPEG dependencies
|
|
||||||
System.loadLibrary("avutil-52");
|
|
||||||
System.loadLibrary("swresample-0");
|
|
||||||
System.loadLibrary("swscale-2");
|
|
||||||
System.loadLibrary("avcodec-55");
|
|
||||||
System.loadLibrary("avformat-55");
|
|
||||||
|
|
||||||
System.loadLibrary("nv_avc_dec");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Disables the deblocking filter at the cost of image quality */
|
|
||||||
public static final int DISABLE_LOOP_FILTER = 0x1;
|
|
||||||
/** Uses the low latency decode flag (disables multithreading) */
|
|
||||||
public static final int LOW_LATENCY_DECODE = 0x2;
|
|
||||||
/** Threads process each slice, rather than each frame */
|
|
||||||
public static final int SLICE_THREADING = 0x4;
|
|
||||||
/** Uses nonstandard speedup tricks */
|
|
||||||
public static final int FAST_DECODE = 0x8;
|
|
||||||
/** Uses bilinear filtering instead of bicubic */
|
|
||||||
public static final int BILINEAR_FILTERING = 0x10;
|
|
||||||
/** Uses a faster bilinear filtering with lower image quality */
|
|
||||||
public static final int FAST_BILINEAR_FILTERING = 0x20;
|
|
||||||
/** Disables color conversion (output is NV21) */
|
|
||||||
public static final int NO_COLOR_CONVERSION = 0x40;
|
|
||||||
|
|
||||||
public static native int init(int width, int height, int perflvl, int threadcount);
|
|
||||||
public static native void destroy();
|
|
||||||
|
|
||||||
// Rendering API when NO_COLOR_CONVERSION == 0
|
|
||||||
public static native boolean setRenderTarget(Object androidSurface);
|
|
||||||
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
|
|
||||||
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
|
|
||||||
public static native boolean redraw();
|
|
||||||
|
|
||||||
// Rendering API when NO_COLOR_CONVERSION == 1
|
|
||||||
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
|
|
||||||
|
|
||||||
public static native int getInputPaddingSize();
|
|
||||||
public static native int decode(byte[] indata, int inoff, int inlen);
|
|
||||||
}
|
|
||||||
@@ -2,33 +2,36 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.View;
|
import android.view.KeyEvent;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.widget.Button;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class AddComputerManually extends Activity {
|
public class AddComputerManually extends Activity {
|
||||||
private Button addPcButton;
|
|
||||||
private TextView hostText;
|
private TextView hostText;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
||||||
private Thread addThread;
|
private Thread addThread;
|
||||||
private ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||||
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
startAddThread();
|
startAddThread();
|
||||||
@@ -43,20 +46,26 @@ public class AddComputerManually extends Activity {
|
|||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
String msg;
|
||||||
boolean finish = false;
|
boolean finish = false;
|
||||||
|
|
||||||
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InetAddress addr = InetAddress.getByName(host);
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
|
|
||||||
if (!managerBinder.addComputerBlocking(addr)){
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
msg = "Successfully added computer";
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
finish = true;
|
finish = true;
|
||||||
}
|
}
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
|
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
final boolean toastFinish = finish;
|
final boolean toastFinish = finish;
|
||||||
final String toastMsg = msg;
|
final String toastMsg = msg;
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
@@ -110,6 +119,7 @@ public class AddComputerManually extends Activity {
|
|||||||
super.onStop();
|
super.onStop();
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -126,28 +136,46 @@ public class AddComputerManually extends Activity {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_add_computer_manually);
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.addPcButton = (Button) findViewById(R.id.addPc);
|
|
||||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||||
|
(keyEvent != null &&
|
||||||
|
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||||
|
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||||
|
if (hostText.getText().length() == 0) {
|
||||||
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
computersToAdd.add(hostText.getText().toString().trim());
|
||||||
|
}
|
||||||
|
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||||
|
// This is how the Fire TV dismisses the keyboard
|
||||||
|
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
// Bind to the ComputerManager service
|
||||||
bindService(new Intent(AddComputerManually.this,
|
bindService(new Intent(AddComputerManually.this,
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
|
|
||||||
addPcButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (hostText.getText().length() == 0) {
|
|
||||||
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
|
|
||||||
computersToAdd.add(hostText.getText().toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,38 +2,62 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
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";
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||||
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
||||||
|
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||||
|
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||||
|
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
||||||
|
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||||
|
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
||||||
|
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
||||||
|
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||||
|
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||||
|
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||||
private static final int BITRATE_DEFAULT_1080_60 = 30;
|
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||||
|
private static final int BITRATE_DEFAULT_4K_30 = 40;
|
||||||
|
private static final int BITRATE_DEFAULT_4K_60 = 80;
|
||||||
|
|
||||||
private static final String DEFAULT_RES_FPS = "720p60";
|
private static final String DEFAULT_RES_FPS = "720p60";
|
||||||
private static final String DEFAULT_DECODER = "auto";
|
|
||||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||||
private static final boolean DEFAULT_STRETCH = false;
|
private static final boolean DEFAULT_STRETCH = false;
|
||||||
private static final boolean DEFAULT_SOPS = true;
|
private static final boolean DEFAULT_SOPS = true;
|
||||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||||
private static final boolean DEFAULT_HOST_AUDIO = false;
|
private static final boolean DEFAULT_HOST_AUDIO = false;
|
||||||
|
private static final int DEFAULT_DEADZONE = 15;
|
||||||
|
public static final String DEFAULT_LANGUAGE = "default";
|
||||||
|
private static final boolean DEFAULT_LIST_MODE = false;
|
||||||
|
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
||||||
|
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
||||||
|
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||||
|
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||||
|
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||||
|
|
||||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
public static final int FORCE_H265_ON = -1;
|
||||||
public static final int AUTOSELECT_DECODER = 0;
|
public static final int AUTOSELECT_H265 = 0;
|
||||||
public static final int FORCE_SOFTWARE_DECODER = 1;
|
public static final int FORCE_H265_OFF = 1;
|
||||||
|
|
||||||
public int width, height, fps;
|
public int width, height, fps;
|
||||||
public int bitrate;
|
public int bitrate;
|
||||||
public int decoder;
|
public int videoFormat;
|
||||||
|
public int deadzonePercentage;
|
||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||||
|
public String language;
|
||||||
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
|
public boolean onscreenController;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
public static int getDefaultBitrate(String resFpsString) {
|
||||||
if (resFpsString.equals("720p30")) {
|
if (resFpsString.equals("720p30")) {
|
||||||
@@ -48,50 +72,59 @@ public class PreferenceConfiguration {
|
|||||||
else if (resFpsString.equals("1080p60")) {
|
else if (resFpsString.equals("1080p60")) {
|
||||||
return BITRATE_DEFAULT_1080_60;
|
return BITRATE_DEFAULT_1080_60;
|
||||||
}
|
}
|
||||||
|
else if (resFpsString.equals("4K30")) {
|
||||||
|
return BITRATE_DEFAULT_4K_30;
|
||||||
|
}
|
||||||
|
else if (resFpsString.equals("4K60")) {
|
||||||
|
return BITRATE_DEFAULT_4K_60;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
// Should never get here
|
||||||
return DEFAULT_BITRATE;
|
return DEFAULT_BITRATE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean getDefaultSmallMode(Context context) {
|
||||||
|
PackageManager manager = context.getPackageManager();
|
||||||
|
if (manager != null) {
|
||||||
|
// TVs shouldn't use small mode by default
|
||||||
|
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 21 uses LEANBACK instead of TELEVISION
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
|
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
||||||
|
}
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
||||||
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) {
|
private static int getVideoFormatValue(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
|
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
|
||||||
if (str.equals("auto")) {
|
if (str.equals("auto")) {
|
||||||
return AUTOSELECT_DECODER;
|
return AUTOSELECT_H265;
|
||||||
}
|
}
|
||||||
else if (str.equals("software")) {
|
else if (str.equals("forceh265")) {
|
||||||
return FORCE_SOFTWARE_DECODER;
|
return FORCE_H265_ON;
|
||||||
}
|
}
|
||||||
else if (str.equals("hardware")) {
|
else if (str.equals("neverh265")) {
|
||||||
return FORCE_HARDWARE_DECODER;
|
return FORCE_H265_OFF;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
// Should never get here
|
||||||
return AUTOSELECT_DECODER;
|
return AUTOSELECT_H265;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +154,16 @@ public class PreferenceConfiguration {
|
|||||||
config.height = 1080;
|
config.height = 1080;
|
||||||
config.fps = 60;
|
config.fps = 60;
|
||||||
}
|
}
|
||||||
|
else if (str.equals("4K30")) {
|
||||||
|
config.width = 3840;
|
||||||
|
config.height = 2160;
|
||||||
|
config.fps = 30;
|
||||||
|
}
|
||||||
|
else if (str.equals("4K60")) {
|
||||||
|
config.width = 3840;
|
||||||
|
config.height = 2160;
|
||||||
|
config.fps = 60;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
// Should never get here
|
||||||
config.width = 1280;
|
config.width = 1280;
|
||||||
@@ -128,13 +171,23 @@ public class PreferenceConfiguration {
|
|||||||
config.fps = 60;
|
config.fps = 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.decoder = getDecoderValue(context);
|
config.videoFormat = getVideoFormatValue(context);
|
||||||
|
|
||||||
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
|
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
|
||||||
|
|
||||||
// Checkbox preferences
|
// Checkbox preferences
|
||||||
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
||||||
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||||
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||||
|
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
||||||
|
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||||
|
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||||
|
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||||
|
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
|
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||||
|
|
||||||
private SeekBar seekBar;
|
private SeekBar seekBar;
|
||||||
private TextView splashText, valueText;
|
private TextView valueText;
|
||||||
private Context context;
|
private final Context context;
|
||||||
|
|
||||||
private String dialogMessage, suffix;
|
private final String dialogMessage;
|
||||||
private int defaultValue, maxValue, currentValue;
|
private final String suffix;
|
||||||
|
private final int defaultValue;
|
||||||
|
private final int maxValue;
|
||||||
|
private final int minValue;
|
||||||
|
private int currentValue;
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
@@ -47,9 +51,10 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
suffix = context.getString(suffixId);
|
suffix = context.getString(suffixId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default and max seekbar values
|
// Get default, min, and max seekbar values
|
||||||
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
|
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||||
|
minValue = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,7 +65,7 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
layout.setOrientation(LinearLayout.VERTICAL);
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
layout.setPadding(6, 6, 6, 6);
|
layout.setPadding(6, 6, 6, 6);
|
||||||
|
|
||||||
splashText = new TextView(context);
|
TextView splashText = new TextView(context);
|
||||||
splashText.setPadding(30, 10, 30, 10);
|
splashText.setPadding(30, 10, 30, 10);
|
||||||
if (dialogMessage != null) {
|
if (dialogMessage != null) {
|
||||||
splashText.setText(dialogMessage);
|
splashText.setText(dialogMessage);
|
||||||
@@ -71,7 +76,7 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||||
valueText.setTextSize(32);
|
valueText.setTextSize(32);
|
||||||
params = new LinearLayout.LayoutParams(
|
params = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.FILL_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
layout.addView(valueText, params);
|
layout.addView(valueText, params);
|
||||||
|
|
||||||
@@ -79,8 +84,13 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
|
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
|
||||||
|
if (value < minValue) {
|
||||||
|
seekBar.setProgress(minValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t = String.valueOf(value);
|
||||||
valueText.setText(suffix == null ? t : t.concat(" " + suffix));
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -90,7 +100,7 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
if (shouldPersist()) {
|
if (shouldPersist()) {
|
||||||
currentValue = getPersistedInt(defaultValue);
|
currentValue = getPersistedInt(defaultValue);
|
||||||
@@ -121,13 +131,6 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMax(int max) {
|
|
||||||
this.maxValue = max;
|
|
||||||
}
|
|
||||||
public int getMax() {
|
|
||||||
return this.maxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgress(int progress) {
|
public void setProgress(int progress) {
|
||||||
this.currentValue = progress;
|
this.currentValue = progress;
|
||||||
if (seekBar != null) {
|
if (seekBar != null) {
|
||||||
@@ -149,10 +152,10 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
if (shouldPersist()) {
|
if (shouldPersist()) {
|
||||||
currentValue = seekBar.getProgress();
|
currentValue = seekBar.getProgress();
|
||||||
persistInt(seekBar.getProgress());
|
persistInt(seekBar.getProgress());
|
||||||
callChangeListener(Integer.valueOf(seekBar.getProgress()));
|
callChangeListener(seekBar.getProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
((AlertDialog) getDialog()).dismiss();
|
getDialog().dismiss();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.preference.CheckBoxPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
public class SmallIconCheckboxPreference extends CheckBoxPreference {
|
||||||
|
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object onGetDefaultValue(TypedArray a, int index) {
|
||||||
|
return PreferenceConfiguration.getDefaultSmallMode(getContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,37 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
|
||||||
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
|
|
||||||
|
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(previousPrefs.language);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
setContentView(R.layout.activity_stream_settings);
|
||||||
getFragmentManager().beginTransaction().replace(
|
getFragmentManager().beginTransaction().replace(
|
||||||
R.id.stream_settings, new SettingsFragment()
|
R.id.stream_settings, new SettingsFragment()
|
||||||
@@ -23,12 +40,37 @@ public class StreamSettings extends Activity {
|
|||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
finish();
|
||||||
|
|
||||||
|
// Check for changes that require a UI reload to take effect
|
||||||
|
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
|
if (newPrefs.listMode != previousPrefs.listMode ||
|
||||||
|
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
|
||||||
|
!newPrefs.language.equals(previousPrefs.language)) {
|
||||||
|
// Restart the PC view to apply UI changes
|
||||||
|
Intent intent = new Intent(this, PcView.class);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
PreferenceScreen screen = getPreferenceScreen();
|
||||||
|
|
||||||
|
// hide on-screen controls category on non touch screen devices
|
||||||
|
if (!getActivity().getPackageManager().
|
||||||
|
hasSystemFeature("android.hardware.touchscreen")) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
screen.removePreference(category);
|
||||||
|
}
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
// Add a listener to the FPS and resolution preference
|
||||||
// so the bitrate can be auto-adjusted
|
// so the bitrate can be auto-adjusted
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
public class AdapterFragment extends Fragment {
|
||||||
|
private AdapterFragmentCallbacks callbacks;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Activity activity) {
|
||||||
|
super.onAttach(activity);
|
||||||
|
|
||||||
|
callbacks = (AdapterFragmentCallbacks) activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
|
public interface AdapterFragmentCallbacks {
|
||||||
|
public int getAdapterFragmentLayoutId();
|
||||||
|
public void receiveAbsListView(AbsListView gridView);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
public interface GameGestures {
|
||||||
|
public void showKeyboard();
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
|
||||||
|
public class StreamView extends SurfaceView {
|
||||||
|
private double desiredAspectRatio;
|
||||||
|
|
||||||
|
public void setDesiredAspectRatio(double aspectRatio) {
|
||||||
|
this.desiredAspectRatio = aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(21)
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||||
|
if (desiredAspectRatio == 0) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||||
|
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
|
||||||
|
int measuredHeight, measuredWidth;
|
||||||
|
if (widthSize > heightSize * desiredAspectRatio) {
|
||||||
|
measuredHeight = heightSize;
|
||||||
|
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||||
|
} else {
|
||||||
|
measuredWidth = widthSize;
|
||||||
|
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
|
||||||
|
public class CacheHelper {
|
||||||
|
private static File openPath(boolean createPath, File root, String... path) {
|
||||||
|
File f = root;
|
||||||
|
for (int i = 0; i < path.length; i++) {
|
||||||
|
String component = path[i];
|
||||||
|
|
||||||
|
if (i == path.length - 1) {
|
||||||
|
// This is the file component so now we create parent directories
|
||||||
|
if (createPath) {
|
||||||
|
f.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f = new File(f, component);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getFileSize(File root, String... path) {
|
||||||
|
return openPath(false, root, path).length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteCacheFile(File root, String... path) {
|
||||||
|
return openPath(false, root, path).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean cacheFileExists(File root, String... path) {
|
||||||
|
return openPath(false, root, path).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||||
|
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||||
|
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out, long maxLength) throws IOException {
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while ((bytesRead = in.read(buf)) != -1) {
|
||||||
|
maxLength -= bytesRead;
|
||||||
|
if (maxLength <= 0) {
|
||||||
|
throw new IOException("Stream exceeded max size");
|
||||||
|
}
|
||||||
|
out.write(buf, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||||
|
Reader r = new InputStreamReader(in);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
char[] buf = new char[256];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = r.read(buf)) != -1) {
|
||||||
|
sb.append(buf, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
|
||||||
|
out.write(str.getBytes("UTF-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,21 @@ import java.util.ArrayList;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
public class Dialog implements Runnable {
|
public class Dialog implements Runnable {
|
||||||
private String title, message;
|
private final String title;
|
||||||
private Activity activity;
|
private final String message;
|
||||||
private boolean endAfterDismiss;
|
private final Activity activity;
|
||||||
|
private final boolean endAfterDismiss;
|
||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
@@ -54,7 +58,7 @@ public class Dialog implements Runnable {
|
|||||||
alert.setCancelable(false);
|
alert.setCancelable(false);
|
||||||
alert.setCanceledOnTouchOutside(false);
|
alert.setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(Dialog.this);
|
rundownDialogs.remove(Dialog.this);
|
||||||
@@ -66,6 +70,31 @@ public class Dialog implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
rundownDialogs.remove(Dialog.this);
|
||||||
|
alert.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endAfterDismiss) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
HelpLauncher.launchTroubleshooting(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
alert.setOnShowListener(new DialogInterface.OnShowListener(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShow(DialogInterface dialog) {
|
||||||
|
// Set focus to the OK button by default
|
||||||
|
Button button = alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
|
button.setFocusable(true);
|
||||||
|
button.setFocusableInTouchMode(true);
|
||||||
|
button.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.add(this);
|
rundownDialogs.add(this);
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.HelpActivity;
|
||||||
|
|
||||||
|
public class HelpLauncher {
|
||||||
|
|
||||||
|
private static void launchUrl(Context context, String url) {
|
||||||
|
// Try to launch the default browser
|
||||||
|
try {
|
||||||
|
// Fire TV devices will lie and say they do have a browser
|
||||||
|
// even though the OS just shows an error dialog if we
|
||||||
|
// try to use it.
|
||||||
|
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse(url));
|
||||||
|
context.startActivity(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// This is only supposed to throw ActivityNotFoundException but
|
||||||
|
// it can (at least) also throw SecurityException if a user's default
|
||||||
|
// browser is not exported. We'll catch everything to workaround this.
|
||||||
|
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
// This platform has no browser (possibly a leanback device)
|
||||||
|
// We'll launch our WebView activity
|
||||||
|
Intent i = new Intent(context, HelpActivity.class);
|
||||||
|
i.setData(Uri.parse(url));
|
||||||
|
context.startActivity(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launchSetupGuide(Context context) {
|
||||||
|
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launchTroubleshooting(Context context) {
|
||||||
|
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.Game;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
public class ServerHelper {
|
||||||
|
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp : computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
Intent intent = new Intent(parent, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
|
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
|
||||||
|
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doQuit(final Activity parent,
|
||||||
|
final InetAddress address,
|
||||||
|
final NvApp app,
|
||||||
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
|
final Runnable onComplete) {
|
||||||
|
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(address,
|
||||||
|
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||||
|
} else {
|
||||||
|
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 599) {
|
||||||
|
message = "This session wasn't started by this device," +
|
||||||
|
" so it cannot be quit. End streaming on the original " +
|
||||||
|
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = parent.getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
} finally {
|
||||||
|
if (onComplete != null) {
|
||||||
|
onComplete.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
parent.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ShortcutInfo;
|
||||||
|
import android.content.pm.ShortcutManager;
|
||||||
|
import android.graphics.drawable.Icon;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.AppViewShortcutTrampoline;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ShortcutHelper {
|
||||||
|
|
||||||
|
private final ShortcutManager sm;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public ShortcutHelper(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
sm = context.getSystemService(ShortcutManager.class);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sm = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private void reapShortcutsForDynamicAdd() {
|
||||||
|
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
||||||
|
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
||||||
|
for (ShortcutInfo scut : dynamicShortcuts) {
|
||||||
|
if (maxRankShortcut.getRank() < scut.getRank()) {
|
||||||
|
maxRankShortcut = scut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sm.removeDynamicShortcuts(Collections.singletonList(maxRankShortcut.getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private List<ShortcutInfo> getAllShortcuts() {
|
||||||
|
LinkedList<ShortcutInfo> list = new LinkedList<>();
|
||||||
|
list.addAll(sm.getDynamicShortcuts());
|
||||||
|
list.addAll(sm.getPinnedShortcuts());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private ShortcutInfo getInfoForId(String id) {
|
||||||
|
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||||
|
|
||||||
|
for (ShortcutInfo info : shortcuts) {
|
||||||
|
if (info.getId().equals(id)) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private boolean isExistingDynamicShortcut(String id) {
|
||||||
|
for (ShortcutInfo si : sm.getDynamicShortcuts()) {
|
||||||
|
if (si.getId().equals(id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportShortcutUsed(String id) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
ShortcutInfo sinfo = getInfoForId(id);
|
||||||
|
if (sinfo != null) {
|
||||||
|
sm.reportShortcutUsed(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
Intent i = new Intent(context, AppViewShortcutTrampoline.class);
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
|
||||||
|
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||||
|
.setIntent(i)
|
||||||
|
.setShortLabel(computerName)
|
||||||
|
.setLongLabel(computerName)
|
||||||
|
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||||
|
if (existingSinfo != null) {
|
||||||
|
// Update in place
|
||||||
|
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||||
|
sm.enableShortcuts(Collections.singletonList(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reap shortcuts to make space for this if it's new
|
||||||
|
// NOTE: This CAN'T be an else on the above if, because it's
|
||||||
|
// possible that we have an existing shortcut but it's not a dynamic one.
|
||||||
|
if (!isExistingDynamicShortcut(id)) {
|
||||||
|
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||||
|
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||||
|
// to interact with this PC.
|
||||||
|
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
reapShortcutsForDynamicAdd();
|
||||||
|
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||||
|
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableShortcut(String id, CharSequence reason) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
ShortcutInfo sinfo = getInfoForId(id);
|
||||||
|
if (sinfo != null) {
|
||||||
|
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,15 @@ import android.content.DialogInterface;
|
|||||||
import android.content.DialogInterface.OnCancelListener;
|
import android.content.DialogInterface.OnCancelListener;
|
||||||
|
|
||||||
public class SpinnerDialog implements Runnable,OnCancelListener {
|
public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||||
private String title, message;
|
private final String title;
|
||||||
private Activity activity;
|
private final String message;
|
||||||
|
private final Activity activity;
|
||||||
private ProgressDialog progress;
|
private ProgressDialog progress;
|
||||||
private boolean finish;
|
private final boolean finish;
|
||||||
|
|
||||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
@@ -14,7 +18,7 @@ public class UiHelper {
|
|||||||
|
|
||||||
public static void notifyNewRootView(Activity activity)
|
public static void notifyNewRootView(Activity activity)
|
||||||
{
|
{
|
||||||
View rootView = (View) activity.findViewById(android.R.id.content);
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||||
|
|
||||||
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
||||||
@@ -28,4 +32,31 @@ public class UiHelper {
|
|||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||||
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||||
|
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
|
||||||
|
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
|
# Support all modern ABIs
|
||||||
|
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
|
||||||
|
|
||||||
# We want an optimized build
|
# We want an optimized build
|
||||||
APP_OPTIM := release
|
APP_OPTIM := release
|
||||||
@@ -10,4 +10,23 @@ LOCAL_MODULE := evdev_reader
|
|||||||
LOCAL_SRC_FILES := evdev_reader.c
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
LOCAL_LDLIBS := -llog
|
LOCAL_LDLIBS := -llog
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
|
||||||
|
# This next portion of the makefile is mostly copied from build-executable.mk but
|
||||||
|
# creates a binary with the libXXX.so form so the APK will install and drop
|
||||||
|
# the binary correctly.
|
||||||
|
|
||||||
|
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||||
|
LOCAL_MAKEFILE := $(local-makefile)
|
||||||
|
|
||||||
|
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||||
|
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
||||||
|
$(call check-LOCAL_MODULE_FILENAME)
|
||||||
|
|
||||||
|
# we are building target objects
|
||||||
|
my := TARGET_
|
||||||
|
|
||||||
|
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
||||||
|
$(call handle-module-built)
|
||||||
|
|
||||||
|
LOCAL_MODULE_CLASS := EXECUTABLE
|
||||||
|
include $(BUILD_SYSTEM)/build-module.mk
|
||||||
@@ -1,41 +1,54 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <jni.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <linux/input.h>
|
#include <linux/input.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
#define EVDEV_MAX_EVENT_SIZE 24
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
|
|
||||||
const char *path;
|
|
||||||
|
|
||||||
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
#define REL_X 0x00
|
||||||
|
#define REL_Y 0x01
|
||||||
|
#define KEY_Q 16
|
||||||
|
#define BTN_LEFT 0x110
|
||||||
|
#define BTN_GAMEPAD 0x130
|
||||||
|
|
||||||
return open(path, O_RDWR);
|
struct DeviceEntry {
|
||||||
}
|
struct DeviceEntry *next;
|
||||||
|
pthread_t thread;
|
||||||
|
int fd;
|
||||||
|
char devName[128];
|
||||||
|
};
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
static struct DeviceEntry *DeviceListHead;
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
static int grabbing = 1;
|
||||||
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
}
|
static pthread_mutex_t SocketSendLock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static int sock;
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
// This is a small executable that runs in a root shell. It reads input
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
// devices and writes the evdev output packets to a socket. This allows
|
||||||
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
// Moonlight to read input devices without having to muck with changing
|
||||||
}
|
// device permissions or modifying SELinux policy (which is prevented in
|
||||||
|
// Marshmallow anyway).
|
||||||
// has*() and friends are based on Android's EventHub.cpp
|
|
||||||
|
|
||||||
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
static int hasRelAxis(int fd, short axis) {
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
|
||||||
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
||||||
|
|
||||||
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
||||||
@@ -43,17 +56,7 @@ Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobje
|
|||||||
return test_bit(axis, relBitmask);
|
return test_bit(axis, relBitmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
static int hasKey(int fd, short key) {
|
||||||
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];
|
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
||||||
|
|
||||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||||
@@ -61,24 +64,40 @@ Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject t
|
|||||||
return test_bit(key, keyBitmask);
|
return test_bit(key, keyBitmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
static void outputEvdevData(char *data, int dataSize) {
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
char packetBuffer[EVDEV_MAX_EVENT_SIZE + sizeof(dataSize)];
|
||||||
jint ret;
|
|
||||||
jbyte *data;
|
|
||||||
int pollres;
|
|
||||||
struct pollfd pollinfo;
|
|
||||||
|
|
||||||
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
// Copy the full packet into our buffer
|
||||||
if (data == NULL) {
|
memcpy(packetBuffer, &dataSize, sizeof(dataSize));
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
memcpy(&packetBuffer[sizeof(dataSize)], data, dataSize);
|
||||||
"Failed to get byte array");
|
|
||||||
return -1;
|
// Lock to prevent other threads from sending at the same time
|
||||||
|
pthread_mutex_lock(&SocketSendLock);
|
||||||
|
send(sock, packetBuffer, dataSize + sizeof(dataSize), 0);
|
||||||
|
pthread_mutex_unlock(&SocketSendLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
void* pollThreadFunc(void* context) {
|
||||||
{
|
struct DeviceEntry *device = context;
|
||||||
|
struct pollfd pollinfo;
|
||||||
|
int pollres, ret;
|
||||||
|
char data[EVDEV_MAX_EVENT_SIZE];
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
|
||||||
|
|
||||||
|
if (grabbing) {
|
||||||
|
// Exclusively grab the input device (required to make the Android cursor disappear)
|
||||||
|
if (ioctl(device->fd, EVIOCGRAB, 1) < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"EVIOCGRAB failed for %s: %d", device->devName, errno);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
do {
|
||||||
// Unwait every 250 ms to return to caller if the fd is closed
|
// Unwait every 250 ms to return to caller if the fd is closed
|
||||||
pollinfo.fd = fd;
|
pollinfo.fd = device->fd;
|
||||||
pollinfo.events = POLLIN;
|
pollinfo.events = POLLIN;
|
||||||
pollinfo.revents = 0;
|
pollinfo.revents = 0;
|
||||||
pollres = poll(&pollinfo, 1, 250);
|
pollres = poll(&pollinfo, 1, 250);
|
||||||
@@ -87,16 +106,23 @@ Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject thi
|
|||||||
|
|
||||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
// We'll have data available now
|
// We'll have data available now
|
||||||
ret = read(fd, data, sizeof(struct input_event));
|
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"read() failed: %d", errno);
|
"read() failed: %d", errno);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
else if (ret == 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"read() graceful EOF");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
else if (grabbing) {
|
||||||
|
// Write out the data to our client
|
||||||
|
outputEvdevData(data, ret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// There must have been a failure
|
|
||||||
ret = -1;
|
|
||||||
|
|
||||||
if (pollres < 0) {
|
if (pollres < 0) {
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"poll() failed: %d", errno);
|
"poll() failed: %d", errno);
|
||||||
@@ -105,14 +131,282 @@ Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject thi
|
|||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"Unexpected revents: %d", pollinfo.revents);
|
"Unexpected revents: %d", pollinfo.revents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Terminate this thread
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
|
cleanup:
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Closing /dev/input/%s", device->devName);
|
||||||
|
|
||||||
|
// Remove the context from the linked list
|
||||||
|
{
|
||||||
|
struct DeviceEntry *lastEntry;
|
||||||
|
|
||||||
|
// Lock the device list
|
||||||
|
pthread_mutex_lock(&DeviceListLock);
|
||||||
|
|
||||||
|
if (DeviceListHead == device) {
|
||||||
|
DeviceListHead = device->next;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lastEntry = DeviceListHead;
|
||||||
|
while (lastEntry->next != NULL) {
|
||||||
|
if (lastEntry->next == device) {
|
||||||
|
lastEntry->next = device->next;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEntry = lastEntry->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock device list
|
||||||
|
pthread_mutex_unlock(&DeviceListLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the context
|
||||||
|
ioctl(device->fd, EVIOCGRAB, 0);
|
||||||
|
close(device->fd);
|
||||||
|
free(device);
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int precheckDeviceForPolling(int fd) {
|
||||||
|
int isMouse;
|
||||||
|
int isKeyboard;
|
||||||
|
int isGamepad;
|
||||||
|
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
isMouse = hasRelAxis(fd, REL_X) &&
|
||||||
|
hasRelAxis(fd, REL_Y) &&
|
||||||
|
hasKey(fd, BTN_LEFT);
|
||||||
|
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
isKeyboard = hasKey(fd, KEY_Q);
|
||||||
|
|
||||||
|
isGamepad = hasKey(fd, BTN_GAMEPAD);
|
||||||
|
|
||||||
|
// We only handle keyboards and mice that aren't gamepads
|
||||||
|
return (isMouse || isKeyboard) && !isGamepad;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void startPollForDevice(char* deviceName) {
|
||||||
|
struct DeviceEntry *currentEntry;
|
||||||
|
char fullPath[256];
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
// Lock the device list
|
||||||
|
pthread_mutex_lock(&DeviceListLock);
|
||||||
|
|
||||||
|
// Check if the device is already being polled
|
||||||
|
currentEntry = DeviceListHead;
|
||||||
|
while (currentEntry != NULL) {
|
||||||
|
if (strcmp(currentEntry->devName, deviceName) == 0) {
|
||||||
|
// Already polling this device
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEntry = currentEntry->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the device
|
||||||
|
sprintf(fullPath, "/dev/input/%s", deviceName);
|
||||||
|
fd = open(fullPath, O_RDWR);
|
||||||
|
if (fd < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open %s: %d", fullPath, errno);
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate a context
|
||||||
|
currentEntry = malloc(sizeof(*currentEntry));
|
||||||
|
if (currentEntry == NULL) {
|
||||||
|
close(fd);
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate context
|
||||||
|
currentEntry->fd = fd;
|
||||||
|
strcpy(currentEntry->devName, deviceName);
|
||||||
|
|
||||||
|
// Check if we support polling this device
|
||||||
|
if (!precheckDeviceForPolling(fd)) {
|
||||||
|
// Nope, get out
|
||||||
|
free(currentEntry);
|
||||||
|
close(fd);
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the polling thread
|
||||||
|
if (pthread_create(¤tEntry->thread, NULL, pollThreadFunc, currentEntry) != 0) {
|
||||||
|
free(currentEntry);
|
||||||
|
close(fd);
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue this onto the device list
|
||||||
|
currentEntry->next = DeviceListHead;
|
||||||
|
DeviceListHead = currentEntry;
|
||||||
|
|
||||||
|
unlock:
|
||||||
|
// Unlock and return
|
||||||
|
pthread_mutex_unlock(&DeviceListLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int enumerateDevices(void) {
|
||||||
|
DIR *inputDir;
|
||||||
|
struct dirent *dirEnt;
|
||||||
|
|
||||||
|
inputDir = opendir("/dev/input");
|
||||||
|
if (!inputDir) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open /dev/input: %d", errno);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling each device in /dev/input
|
||||||
|
while ((dirEnt = readdir(inputDir)) != NULL) {
|
||||||
|
if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0) {
|
||||||
|
// Skip these virtual directories
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strstr(dirEnt->d_name, "event") == NULL) {
|
||||||
|
// Skip non-event devices
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
startPollForDevice(dirEnt->d_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(inputDir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int connectSocket(int port) {
|
||||||
|
struct sockaddr_in saddr;
|
||||||
|
int ret;
|
||||||
|
int val;
|
||||||
|
|
||||||
|
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
|
if (sock < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "socket() failed: %d", errno);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&saddr, 0, sizeof(saddr));
|
||||||
|
saddr.sin_family = AF_INET;
|
||||||
|
saddr.sin_port = htons(port);
|
||||||
|
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||||
|
ret = connect(sock, (struct sockaddr*)&saddr, sizeof(saddr));
|
||||||
|
if (ret < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "connect() failed: %d", errno);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
val = 1;
|
||||||
|
ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
|
||||||
|
if (ret < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "setsockopt() failed: %d", errno);
|
||||||
|
// We can continue anyways
|
||||||
|
}
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Connection established to port %d", port);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define UNGRAB_REQ 1
|
||||||
|
#define REGRAB_REQ 2
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
int ret;
|
||||||
|
int pollres;
|
||||||
|
struct pollfd pollinfo;
|
||||||
|
int port;
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
|
||||||
|
|
||||||
|
port = atoi(argv[1]);
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
|
||||||
|
|
||||||
|
// Connect to the app's socket
|
||||||
|
ret = connectSocket(port);
|
||||||
|
if (ret < 0) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
// Perform initial enumeration
|
||||||
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
|
ret = enumerateDevices();
|
||||||
return close(fd);
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for requests from the client
|
||||||
|
for (;;) {
|
||||||
|
unsigned char requestId;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Every second we poll again for new devices if
|
||||||
|
// we haven't received any new events
|
||||||
|
pollinfo.fd = sock;
|
||||||
|
pollinfo.events = POLLIN;
|
||||||
|
pollinfo.revents = 0;
|
||||||
|
pollres = poll(&pollinfo, 1, 1000);
|
||||||
|
if (pollres == 0) {
|
||||||
|
// Timeout, re-enumerate devices
|
||||||
|
enumerateDevices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (pollres == 0);
|
||||||
|
|
||||||
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
|
// We'll have data available now
|
||||||
|
ret = recv(sock, &requestId, sizeof(requestId), 0);
|
||||||
|
if (ret < sizeof(requestId)) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
||||||
|
return requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
struct DeviceEntry *currentEntry;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&DeviceListLock);
|
||||||
|
|
||||||
|
// Update state for future devices
|
||||||
|
grabbing = (requestId == REGRAB_REQ);
|
||||||
|
|
||||||
|
// Carry out the requested action on each device
|
||||||
|
currentEntry = DeviceListHead;
|
||||||
|
while (currentEntry != NULL) {
|
||||||
|
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
||||||
|
currentEntry = currentEntry->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&DeviceListLock);
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "New grab status is: %s",
|
||||||
|
grabbing ? "enabled" : "disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Terminate this thread
|
||||||
|
if (pollres < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Socket recv poll() failed: %d", errno);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Socket poll unexpected revents: %d", pollinfo.revents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Android.mk for Moonlight's ENet JNI binding
|
||||||
|
MY_LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
|
include $(call all-subdir-makefiles)
|
||||||
|
|
||||||
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE := jnienet
|
||||||
|
|
||||||
|
LOCAL_SRC_FILES := jnienet.c \
|
||||||
|
enet/callbacks.c \
|
||||||
|
enet/compress.c \
|
||||||
|
enet/host.c \
|
||||||
|
enet/list.c \
|
||||||
|
enet/packet.c \
|
||||||
|
enet/peer.c \
|
||||||
|
enet/protocol.c \
|
||||||
|
enet/unix.c \
|
||||||
|
enet/win32.c \
|
||||||
|
|
||||||
|
LOCAL_CFLAGS := -DHAS_SOCKLEN_T=1
|
||||||
|
LOCAL_C_INCLUDES := $(LOCAL_PATH)/enet/include
|
||||||
|
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
Submodule
+1
Submodule app/src/main/jni/jnienet/enet added at 7546b505c1.
@@ -0,0 +1,148 @@
|
|||||||
|
#include "enet/enet.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#define CLIENT_TO_LONG(x) ((intptr_t)(x))
|
||||||
|
#define LONG_TO_CLIENT(x) ((ENetHost*)(intptr_t)(x))
|
||||||
|
|
||||||
|
#define PEER_TO_LONG(x) ((intptr_t)(x))
|
||||||
|
#define LONG_TO_PEER(x) ((ENetPeer*)(intptr_t)(x))
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_initializeEnet(JNIEnv *env, jobject class) {
|
||||||
|
return enet_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_createClient(JNIEnv *env, jobject class, jstring address) {
|
||||||
|
ENetAddress enetAddress;
|
||||||
|
const char *addrStr;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
// Perform a lookup on the address to determine the address family
|
||||||
|
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||||
|
err = enet_address_set_host(&enetAddress, addrStr);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||||
|
if (err < 0) {
|
||||||
|
return CLIENT_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client that can use 1 outgoing connection and 1 channel
|
||||||
|
return CLIENT_TO_LONG(enet_host_create(enetAddress.address.ss_family, NULL, 1, 1, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_connectToPeer(JNIEnv *env, jobject class, jlong client, jstring address, jint port, jint timeout) {
|
||||||
|
ENetPeer* peer;
|
||||||
|
ENetAddress enetAddress;
|
||||||
|
ENetEvent event;
|
||||||
|
const char *addrStr;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
// Initialize the ENet address
|
||||||
|
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||||
|
err = enet_address_set_host(&enetAddress, addrStr);
|
||||||
|
enet_address_set_port(&enetAddress, port);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||||
|
if (err < 0) {
|
||||||
|
return PEER_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the connection
|
||||||
|
peer = enet_host_connect(LONG_TO_CLIENT(client), &enetAddress, 1, 0);
|
||||||
|
if (peer == NULL) {
|
||||||
|
return PEER_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the connect to complete
|
||||||
|
if (enet_host_service(LONG_TO_CLIENT(client), &event, timeout) <= 0 || event.type != ENET_EVENT_TYPE_CONNECT) {
|
||||||
|
enet_peer_reset(peer);
|
||||||
|
return PEER_TO_LONG(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the connect verify ACK is sent immediately
|
||||||
|
enet_host_flush(LONG_TO_CLIENT(client));
|
||||||
|
|
||||||
|
// Set the max peer timeout to 10 seconds
|
||||||
|
enet_peer_timeout(peer, ENET_PEER_TIMEOUT_LIMIT, ENET_PEER_TIMEOUT_MINIMUM, 10000);
|
||||||
|
|
||||||
|
return PEER_TO_LONG(peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_readPacket(JNIEnv *env, jobject class, jlong client, jbyteArray data, jint length, jint timeout) {
|
||||||
|
jint err;
|
||||||
|
jbyte* dataPtr;
|
||||||
|
ENetEvent event;
|
||||||
|
|
||||||
|
// Wait for a receive event, timeout, or disconnect
|
||||||
|
err = enet_host_service(LONG_TO_CLIENT(client), &event, timeout);
|
||||||
|
if (err <= 0) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
else if (event.type != ENET_EVENT_TYPE_RECEIVE) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the packet isn't too large
|
||||||
|
if (event.packet->dataLength > length) {
|
||||||
|
enet_packet_destroy(event.packet);
|
||||||
|
return event.packet->dataLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the packet data into the caller's buffer
|
||||||
|
dataPtr = (*env)->GetByteArrayElements(env, data, 0);
|
||||||
|
memcpy(dataPtr, event.packet->data, event.packet->dataLength);
|
||||||
|
err = event.packet->dataLength;
|
||||||
|
(*env)->ReleaseByteArrayElements(env, data, dataPtr, 0);
|
||||||
|
|
||||||
|
// Free the packet
|
||||||
|
enet_packet_destroy(event.packet);
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_writePacket(JNIEnv *env, jobject class, jlong client, jlong peer, jbyteArray data, jint length, jint packetFlags) {
|
||||||
|
ENetPacket* packet;
|
||||||
|
jboolean ret;
|
||||||
|
jbyte* dataPtr;
|
||||||
|
|
||||||
|
dataPtr = (*env)->GetByteArrayElements(env, data, 0);
|
||||||
|
|
||||||
|
// Create the reliable packet that describes our outgoing message
|
||||||
|
packet = enet_packet_create(dataPtr, length, packetFlags);
|
||||||
|
if (packet != NULL) {
|
||||||
|
// Send the message to the peer
|
||||||
|
if (enet_peer_send(LONG_TO_PEER(peer), 0, packet) < 0) {
|
||||||
|
// This can fail if the peer has been disconnected
|
||||||
|
enet_packet_destroy(packet);
|
||||||
|
ret = JNI_FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Force the client to send the packet now
|
||||||
|
enet_host_flush(LONG_TO_CLIENT(client));
|
||||||
|
ret = JNI_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret = JNI_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->ReleaseByteArrayElements(env, data, dataPtr, JNI_ABORT);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_destroyClient(JNIEnv *env, jobject class, jlong client) {
|
||||||
|
enet_host_destroy(LONG_TO_CLIENT(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_limelight_nvstream_enet_EnetConnection_disconnectPeer(JNIEnv *env, jobject class, jlong peer) {
|
||||||
|
enet_peer_disconnect_now(LONG_TO_PEER(peer), 0);
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Android.mk for Limelight's H264 decoder
|
|
||||||
MY_LOCAL_PATH := $(call my-dir)
|
|
||||||
|
|
||||||
include $(call all-subdir-makefiles)
|
|
||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE := nv_avc_dec
|
|
||||||
LOCAL_SRC_FILES := nv_avc_dec.c nv_avc_dec_jni.c
|
|
||||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/$(TARGET_ARCH_ABI)/include
|
|
||||||
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
|
|
||||||
|
|
||||||
# Link to ffmpeg libraries
|
|
||||||
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libwsresample
|
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
LOCAL_PATH:= $(call my-dir)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE:= libavcodec
|
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavcodec-55.so
|
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE:= libavformat
|
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavformat-55.so
|
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE:= libswscale
|
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswscale-2.so
|
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE:= libavutil
|
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavutil-52.so
|
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE:= libwsresample
|
|
||||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
|
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_AVFFT_H
|
|
||||||
#define AVCODEC_AVFFT_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup lavc_fft
|
|
||||||
* FFT functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @defgroup lavc_fft FFT functions
|
|
||||||
* @ingroup lavc_misc
|
|
||||||
*
|
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
|
|
||||||
typedef float FFTSample;
|
|
||||||
|
|
||||||
typedef struct FFTComplex {
|
|
||||||
FFTSample re, im;
|
|
||||||
} FFTComplex;
|
|
||||||
|
|
||||||
typedef struct FFTContext FFTContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up a complex FFT.
|
|
||||||
* @param nbits log2 of the length of the input array
|
|
||||||
* @param inverse if 0 perform the forward transform, if 1 perform the inverse
|
|
||||||
*/
|
|
||||||
FFTContext *av_fft_init(int nbits, int inverse);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do the permutation needed BEFORE calling ff_fft_calc().
|
|
||||||
*/
|
|
||||||
void av_fft_permute(FFTContext *s, FFTComplex *z);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do a complex FFT with the parameters defined in av_fft_init(). The
|
|
||||||
* input data must be permuted before. No 1.0/sqrt(n) normalization is done.
|
|
||||||
*/
|
|
||||||
void av_fft_calc(FFTContext *s, FFTComplex *z);
|
|
||||||
|
|
||||||
void av_fft_end(FFTContext *s);
|
|
||||||
|
|
||||||
FFTContext *av_mdct_init(int nbits, int inverse, double scale);
|
|
||||||
void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
|
|
||||||
void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
|
|
||||||
void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
|
|
||||||
void av_mdct_end(FFTContext *s);
|
|
||||||
|
|
||||||
/* Real Discrete Fourier Transform */
|
|
||||||
|
|
||||||
enum RDFTransformType {
|
|
||||||
DFT_R2C,
|
|
||||||
IDFT_C2R,
|
|
||||||
IDFT_R2C,
|
|
||||||
DFT_C2R,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct RDFTContext RDFTContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up a real FFT.
|
|
||||||
* @param nbits log2 of the length of the input array
|
|
||||||
* @param trans the type of transform
|
|
||||||
*/
|
|
||||||
RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
|
|
||||||
void av_rdft_calc(RDFTContext *s, FFTSample *data);
|
|
||||||
void av_rdft_end(RDFTContext *s);
|
|
||||||
|
|
||||||
/* Discrete Cosine Transform */
|
|
||||||
|
|
||||||
typedef struct DCTContext DCTContext;
|
|
||||||
|
|
||||||
enum DCTTransformType {
|
|
||||||
DCT_II = 0,
|
|
||||||
DCT_III,
|
|
||||||
DCT_I,
|
|
||||||
DST_I,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up DCT.
|
|
||||||
* @param nbits size of the input array:
|
|
||||||
* (1 << nbits) for DCT-II, DCT-III and DST-I
|
|
||||||
* (1 << nbits) + 1 for DCT-I
|
|
||||||
*
|
|
||||||
* @note the first element of the input of DST-I is ignored
|
|
||||||
*/
|
|
||||||
DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
|
|
||||||
void av_dct_calc(DCTContext *s, FFTSample *data);
|
|
||||||
void av_dct_end (DCTContext *s);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#endif /* AVCODEC_AVFFT_H */
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* DXVA2 HW acceleration
|
|
||||||
*
|
|
||||||
* copyright (c) 2009 Laurent Aimar
|
|
||||||
*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_DXVA_H
|
|
||||||
#define AVCODEC_DXVA_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup lavc_codec_hwaccel_dxva2
|
|
||||||
* Public libavcodec DXVA2 header.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0600
|
|
||||||
#undef _WIN32_WINNT
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if !defined(_WIN32_WINNT)
|
|
||||||
#define _WIN32_WINNT 0x0600
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <d3d9.h>
|
|
||||||
#include <dxva2api.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @defgroup lavc_codec_hwaccel_dxva2 DXVA2
|
|
||||||
* @ingroup lavc_codec_hwaccel
|
|
||||||
*
|
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This structure is used to provides the necessary configurations and data
|
|
||||||
* to the DXVA2 FFmpeg HWAccel implementation.
|
|
||||||
*
|
|
||||||
* The application must make it available as AVCodecContext.hwaccel_context.
|
|
||||||
*/
|
|
||||||
struct dxva_context {
|
|
||||||
/**
|
|
||||||
* DXVA2 decoder object
|
|
||||||
*/
|
|
||||||
IDirectXVideoDecoder *decoder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DXVA2 configuration used to create the decoder
|
|
||||||
*/
|
|
||||||
const DXVA2_ConfigPictureDecode *cfg;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of surface in the surface array
|
|
||||||
*/
|
|
||||||
unsigned surface_count;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The array of Direct3D surfaces used to create the decoder
|
|
||||||
*/
|
|
||||||
LPDIRECT3DSURFACE9 *surface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bit field configuring the workarounds needed for using the decoder
|
|
||||||
*/
|
|
||||||
uint64_t workaround;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private to the FFmpeg AVHWAccel implementation
|
|
||||||
*/
|
|
||||||
unsigned report_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#endif /* AVCODEC_DXVA_H */
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_OLD_CODEC_IDS_H
|
|
||||||
#define AVCODEC_OLD_CODEC_IDS_H
|
|
||||||
|
|
||||||
#include "libavutil/common.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This header exists to prevent new codec IDs from being accidentally added to
|
|
||||||
* the deprecated list.
|
|
||||||
* Do not include it directly. It will be removed on next major bump
|
|
||||||
*
|
|
||||||
* Do not add new items to this list. Use the AVCodecID enum instead.
|
|
||||||
*/
|
|
||||||
|
|
||||||
CODEC_ID_NONE = AV_CODEC_ID_NONE,
|
|
||||||
|
|
||||||
/* video codecs */
|
|
||||||
CODEC_ID_MPEG1VIDEO,
|
|
||||||
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
|
||||||
CODEC_ID_MPEG2VIDEO_XVMC,
|
|
||||||
CODEC_ID_H261,
|
|
||||||
CODEC_ID_H263,
|
|
||||||
CODEC_ID_RV10,
|
|
||||||
CODEC_ID_RV20,
|
|
||||||
CODEC_ID_MJPEG,
|
|
||||||
CODEC_ID_MJPEGB,
|
|
||||||
CODEC_ID_LJPEG,
|
|
||||||
CODEC_ID_SP5X,
|
|
||||||
CODEC_ID_JPEGLS,
|
|
||||||
CODEC_ID_MPEG4,
|
|
||||||
CODEC_ID_RAWVIDEO,
|
|
||||||
CODEC_ID_MSMPEG4V1,
|
|
||||||
CODEC_ID_MSMPEG4V2,
|
|
||||||
CODEC_ID_MSMPEG4V3,
|
|
||||||
CODEC_ID_WMV1,
|
|
||||||
CODEC_ID_WMV2,
|
|
||||||
CODEC_ID_H263P,
|
|
||||||
CODEC_ID_H263I,
|
|
||||||
CODEC_ID_FLV1,
|
|
||||||
CODEC_ID_SVQ1,
|
|
||||||
CODEC_ID_SVQ3,
|
|
||||||
CODEC_ID_DVVIDEO,
|
|
||||||
CODEC_ID_HUFFYUV,
|
|
||||||
CODEC_ID_CYUV,
|
|
||||||
CODEC_ID_H264,
|
|
||||||
CODEC_ID_INDEO3,
|
|
||||||
CODEC_ID_VP3,
|
|
||||||
CODEC_ID_THEORA,
|
|
||||||
CODEC_ID_ASV1,
|
|
||||||
CODEC_ID_ASV2,
|
|
||||||
CODEC_ID_FFV1,
|
|
||||||
CODEC_ID_4XM,
|
|
||||||
CODEC_ID_VCR1,
|
|
||||||
CODEC_ID_CLJR,
|
|
||||||
CODEC_ID_MDEC,
|
|
||||||
CODEC_ID_ROQ,
|
|
||||||
CODEC_ID_INTERPLAY_VIDEO,
|
|
||||||
CODEC_ID_XAN_WC3,
|
|
||||||
CODEC_ID_XAN_WC4,
|
|
||||||
CODEC_ID_RPZA,
|
|
||||||
CODEC_ID_CINEPAK,
|
|
||||||
CODEC_ID_WS_VQA,
|
|
||||||
CODEC_ID_MSRLE,
|
|
||||||
CODEC_ID_MSVIDEO1,
|
|
||||||
CODEC_ID_IDCIN,
|
|
||||||
CODEC_ID_8BPS,
|
|
||||||
CODEC_ID_SMC,
|
|
||||||
CODEC_ID_FLIC,
|
|
||||||
CODEC_ID_TRUEMOTION1,
|
|
||||||
CODEC_ID_VMDVIDEO,
|
|
||||||
CODEC_ID_MSZH,
|
|
||||||
CODEC_ID_ZLIB,
|
|
||||||
CODEC_ID_QTRLE,
|
|
||||||
CODEC_ID_TSCC,
|
|
||||||
CODEC_ID_ULTI,
|
|
||||||
CODEC_ID_QDRAW,
|
|
||||||
CODEC_ID_VIXL,
|
|
||||||
CODEC_ID_QPEG,
|
|
||||||
CODEC_ID_PNG,
|
|
||||||
CODEC_ID_PPM,
|
|
||||||
CODEC_ID_PBM,
|
|
||||||
CODEC_ID_PGM,
|
|
||||||
CODEC_ID_PGMYUV,
|
|
||||||
CODEC_ID_PAM,
|
|
||||||
CODEC_ID_FFVHUFF,
|
|
||||||
CODEC_ID_RV30,
|
|
||||||
CODEC_ID_RV40,
|
|
||||||
CODEC_ID_VC1,
|
|
||||||
CODEC_ID_WMV3,
|
|
||||||
CODEC_ID_LOCO,
|
|
||||||
CODEC_ID_WNV1,
|
|
||||||
CODEC_ID_AASC,
|
|
||||||
CODEC_ID_INDEO2,
|
|
||||||
CODEC_ID_FRAPS,
|
|
||||||
CODEC_ID_TRUEMOTION2,
|
|
||||||
CODEC_ID_BMP,
|
|
||||||
CODEC_ID_CSCD,
|
|
||||||
CODEC_ID_MMVIDEO,
|
|
||||||
CODEC_ID_ZMBV,
|
|
||||||
CODEC_ID_AVS,
|
|
||||||
CODEC_ID_SMACKVIDEO,
|
|
||||||
CODEC_ID_NUV,
|
|
||||||
CODEC_ID_KMVC,
|
|
||||||
CODEC_ID_FLASHSV,
|
|
||||||
CODEC_ID_CAVS,
|
|
||||||
CODEC_ID_JPEG2000,
|
|
||||||
CODEC_ID_VMNC,
|
|
||||||
CODEC_ID_VP5,
|
|
||||||
CODEC_ID_VP6,
|
|
||||||
CODEC_ID_VP6F,
|
|
||||||
CODEC_ID_TARGA,
|
|
||||||
CODEC_ID_DSICINVIDEO,
|
|
||||||
CODEC_ID_TIERTEXSEQVIDEO,
|
|
||||||
CODEC_ID_TIFF,
|
|
||||||
CODEC_ID_GIF,
|
|
||||||
CODEC_ID_DXA,
|
|
||||||
CODEC_ID_DNXHD,
|
|
||||||
CODEC_ID_THP,
|
|
||||||
CODEC_ID_SGI,
|
|
||||||
CODEC_ID_C93,
|
|
||||||
CODEC_ID_BETHSOFTVID,
|
|
||||||
CODEC_ID_PTX,
|
|
||||||
CODEC_ID_TXD,
|
|
||||||
CODEC_ID_VP6A,
|
|
||||||
CODEC_ID_AMV,
|
|
||||||
CODEC_ID_VB,
|
|
||||||
CODEC_ID_PCX,
|
|
||||||
CODEC_ID_SUNRAST,
|
|
||||||
CODEC_ID_INDEO4,
|
|
||||||
CODEC_ID_INDEO5,
|
|
||||||
CODEC_ID_MIMIC,
|
|
||||||
CODEC_ID_RL2,
|
|
||||||
CODEC_ID_ESCAPE124,
|
|
||||||
CODEC_ID_DIRAC,
|
|
||||||
CODEC_ID_BFI,
|
|
||||||
CODEC_ID_CMV,
|
|
||||||
CODEC_ID_MOTIONPIXELS,
|
|
||||||
CODEC_ID_TGV,
|
|
||||||
CODEC_ID_TGQ,
|
|
||||||
CODEC_ID_TQI,
|
|
||||||
CODEC_ID_AURA,
|
|
||||||
CODEC_ID_AURA2,
|
|
||||||
CODEC_ID_V210X,
|
|
||||||
CODEC_ID_TMV,
|
|
||||||
CODEC_ID_V210,
|
|
||||||
CODEC_ID_DPX,
|
|
||||||
CODEC_ID_MAD,
|
|
||||||
CODEC_ID_FRWU,
|
|
||||||
CODEC_ID_FLASHSV2,
|
|
||||||
CODEC_ID_CDGRAPHICS,
|
|
||||||
CODEC_ID_R210,
|
|
||||||
CODEC_ID_ANM,
|
|
||||||
CODEC_ID_BINKVIDEO,
|
|
||||||
CODEC_ID_IFF_ILBM,
|
|
||||||
CODEC_ID_IFF_BYTERUN1,
|
|
||||||
CODEC_ID_KGV1,
|
|
||||||
CODEC_ID_YOP,
|
|
||||||
CODEC_ID_VP8,
|
|
||||||
CODEC_ID_PICTOR,
|
|
||||||
CODEC_ID_ANSI,
|
|
||||||
CODEC_ID_A64_MULTI,
|
|
||||||
CODEC_ID_A64_MULTI5,
|
|
||||||
CODEC_ID_R10K,
|
|
||||||
CODEC_ID_MXPEG,
|
|
||||||
CODEC_ID_LAGARITH,
|
|
||||||
CODEC_ID_PRORES,
|
|
||||||
CODEC_ID_JV,
|
|
||||||
CODEC_ID_DFA,
|
|
||||||
CODEC_ID_WMV3IMAGE,
|
|
||||||
CODEC_ID_VC1IMAGE,
|
|
||||||
CODEC_ID_UTVIDEO,
|
|
||||||
CODEC_ID_BMV_VIDEO,
|
|
||||||
CODEC_ID_VBLE,
|
|
||||||
CODEC_ID_DXTORY,
|
|
||||||
CODEC_ID_V410,
|
|
||||||
CODEC_ID_XWD,
|
|
||||||
CODEC_ID_CDXL,
|
|
||||||
CODEC_ID_XBM,
|
|
||||||
CODEC_ID_ZEROCODEC,
|
|
||||||
CODEC_ID_MSS1,
|
|
||||||
CODEC_ID_MSA1,
|
|
||||||
CODEC_ID_TSCC2,
|
|
||||||
CODEC_ID_MTS2,
|
|
||||||
CODEC_ID_CLLC,
|
|
||||||
CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
|
|
||||||
CODEC_ID_ESCAPE130 = MKBETAG('E','1','3','0'),
|
|
||||||
CODEC_ID_EXR = MKBETAG('0','E','X','R'),
|
|
||||||
CODEC_ID_AVRP = MKBETAG('A','V','R','P'),
|
|
||||||
|
|
||||||
CODEC_ID_G2M = MKBETAG( 0 ,'G','2','M'),
|
|
||||||
CODEC_ID_AVUI = MKBETAG('A','V','U','I'),
|
|
||||||
CODEC_ID_AYUV = MKBETAG('A','Y','U','V'),
|
|
||||||
CODEC_ID_V308 = MKBETAG('V','3','0','8'),
|
|
||||||
CODEC_ID_V408 = MKBETAG('V','4','0','8'),
|
|
||||||
CODEC_ID_YUV4 = MKBETAG('Y','U','V','4'),
|
|
||||||
CODEC_ID_SANM = MKBETAG('S','A','N','M'),
|
|
||||||
CODEC_ID_PAF_VIDEO = MKBETAG('P','A','F','V'),
|
|
||||||
CODEC_ID_SNOW = AV_CODEC_ID_SNOW,
|
|
||||||
|
|
||||||
/* various PCM "codecs" */
|
|
||||||
CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
|
||||||
CODEC_ID_PCM_S16LE = 0x10000,
|
|
||||||
CODEC_ID_PCM_S16BE,
|
|
||||||
CODEC_ID_PCM_U16LE,
|
|
||||||
CODEC_ID_PCM_U16BE,
|
|
||||||
CODEC_ID_PCM_S8,
|
|
||||||
CODEC_ID_PCM_U8,
|
|
||||||
CODEC_ID_PCM_MULAW,
|
|
||||||
CODEC_ID_PCM_ALAW,
|
|
||||||
CODEC_ID_PCM_S32LE,
|
|
||||||
CODEC_ID_PCM_S32BE,
|
|
||||||
CODEC_ID_PCM_U32LE,
|
|
||||||
CODEC_ID_PCM_U32BE,
|
|
||||||
CODEC_ID_PCM_S24LE,
|
|
||||||
CODEC_ID_PCM_S24BE,
|
|
||||||
CODEC_ID_PCM_U24LE,
|
|
||||||
CODEC_ID_PCM_U24BE,
|
|
||||||
CODEC_ID_PCM_S24DAUD,
|
|
||||||
CODEC_ID_PCM_ZORK,
|
|
||||||
CODEC_ID_PCM_S16LE_PLANAR,
|
|
||||||
CODEC_ID_PCM_DVD,
|
|
||||||
CODEC_ID_PCM_F32BE,
|
|
||||||
CODEC_ID_PCM_F32LE,
|
|
||||||
CODEC_ID_PCM_F64BE,
|
|
||||||
CODEC_ID_PCM_F64LE,
|
|
||||||
CODEC_ID_PCM_BLURAY,
|
|
||||||
CODEC_ID_PCM_LXF,
|
|
||||||
CODEC_ID_S302M,
|
|
||||||
CODEC_ID_PCM_S8_PLANAR,
|
|
||||||
|
|
||||||
/* various ADPCM codecs */
|
|
||||||
CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
|
||||||
CODEC_ID_ADPCM_IMA_WAV,
|
|
||||||
CODEC_ID_ADPCM_IMA_DK3,
|
|
||||||
CODEC_ID_ADPCM_IMA_DK4,
|
|
||||||
CODEC_ID_ADPCM_IMA_WS,
|
|
||||||
CODEC_ID_ADPCM_IMA_SMJPEG,
|
|
||||||
CODEC_ID_ADPCM_MS,
|
|
||||||
CODEC_ID_ADPCM_4XM,
|
|
||||||
CODEC_ID_ADPCM_XA,
|
|
||||||
CODEC_ID_ADPCM_ADX,
|
|
||||||
CODEC_ID_ADPCM_EA,
|
|
||||||
CODEC_ID_ADPCM_G726,
|
|
||||||
CODEC_ID_ADPCM_CT,
|
|
||||||
CODEC_ID_ADPCM_SWF,
|
|
||||||
CODEC_ID_ADPCM_YAMAHA,
|
|
||||||
CODEC_ID_ADPCM_SBPRO_4,
|
|
||||||
CODEC_ID_ADPCM_SBPRO_3,
|
|
||||||
CODEC_ID_ADPCM_SBPRO_2,
|
|
||||||
CODEC_ID_ADPCM_THP,
|
|
||||||
CODEC_ID_ADPCM_IMA_AMV,
|
|
||||||
CODEC_ID_ADPCM_EA_R1,
|
|
||||||
CODEC_ID_ADPCM_EA_R3,
|
|
||||||
CODEC_ID_ADPCM_EA_R2,
|
|
||||||
CODEC_ID_ADPCM_IMA_EA_SEAD,
|
|
||||||
CODEC_ID_ADPCM_IMA_EA_EACS,
|
|
||||||
CODEC_ID_ADPCM_EA_XAS,
|
|
||||||
CODEC_ID_ADPCM_EA_MAXIS_XA,
|
|
||||||
CODEC_ID_ADPCM_IMA_ISS,
|
|
||||||
CODEC_ID_ADPCM_G722,
|
|
||||||
CODEC_ID_ADPCM_IMA_APC,
|
|
||||||
CODEC_ID_VIMA = MKBETAG('V','I','M','A'),
|
|
||||||
|
|
||||||
/* AMR */
|
|
||||||
CODEC_ID_AMR_NB = 0x12000,
|
|
||||||
CODEC_ID_AMR_WB,
|
|
||||||
|
|
||||||
/* RealAudio codecs*/
|
|
||||||
CODEC_ID_RA_144 = 0x13000,
|
|
||||||
CODEC_ID_RA_288,
|
|
||||||
|
|
||||||
/* various DPCM codecs */
|
|
||||||
CODEC_ID_ROQ_DPCM = 0x14000,
|
|
||||||
CODEC_ID_INTERPLAY_DPCM,
|
|
||||||
CODEC_ID_XAN_DPCM,
|
|
||||||
CODEC_ID_SOL_DPCM,
|
|
||||||
|
|
||||||
/* audio codecs */
|
|
||||||
CODEC_ID_MP2 = 0x15000,
|
|
||||||
CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
|
|
||||||
CODEC_ID_AAC,
|
|
||||||
CODEC_ID_AC3,
|
|
||||||
CODEC_ID_DTS,
|
|
||||||
CODEC_ID_VORBIS,
|
|
||||||
CODEC_ID_DVAUDIO,
|
|
||||||
CODEC_ID_WMAV1,
|
|
||||||
CODEC_ID_WMAV2,
|
|
||||||
CODEC_ID_MACE3,
|
|
||||||
CODEC_ID_MACE6,
|
|
||||||
CODEC_ID_VMDAUDIO,
|
|
||||||
CODEC_ID_FLAC,
|
|
||||||
CODEC_ID_MP3ADU,
|
|
||||||
CODEC_ID_MP3ON4,
|
|
||||||
CODEC_ID_SHORTEN,
|
|
||||||
CODEC_ID_ALAC,
|
|
||||||
CODEC_ID_WESTWOOD_SND1,
|
|
||||||
CODEC_ID_GSM, ///< as in Berlin toast format
|
|
||||||
CODEC_ID_QDM2,
|
|
||||||
CODEC_ID_COOK,
|
|
||||||
CODEC_ID_TRUESPEECH,
|
|
||||||
CODEC_ID_TTA,
|
|
||||||
CODEC_ID_SMACKAUDIO,
|
|
||||||
CODEC_ID_QCELP,
|
|
||||||
CODEC_ID_WAVPACK,
|
|
||||||
CODEC_ID_DSICINAUDIO,
|
|
||||||
CODEC_ID_IMC,
|
|
||||||
CODEC_ID_MUSEPACK7,
|
|
||||||
CODEC_ID_MLP,
|
|
||||||
CODEC_ID_GSM_MS, /* as found in WAV */
|
|
||||||
CODEC_ID_ATRAC3,
|
|
||||||
CODEC_ID_VOXWARE,
|
|
||||||
CODEC_ID_APE,
|
|
||||||
CODEC_ID_NELLYMOSER,
|
|
||||||
CODEC_ID_MUSEPACK8,
|
|
||||||
CODEC_ID_SPEEX,
|
|
||||||
CODEC_ID_WMAVOICE,
|
|
||||||
CODEC_ID_WMAPRO,
|
|
||||||
CODEC_ID_WMALOSSLESS,
|
|
||||||
CODEC_ID_ATRAC3P,
|
|
||||||
CODEC_ID_EAC3,
|
|
||||||
CODEC_ID_SIPR,
|
|
||||||
CODEC_ID_MP1,
|
|
||||||
CODEC_ID_TWINVQ,
|
|
||||||
CODEC_ID_TRUEHD,
|
|
||||||
CODEC_ID_MP4ALS,
|
|
||||||
CODEC_ID_ATRAC1,
|
|
||||||
CODEC_ID_BINKAUDIO_RDFT,
|
|
||||||
CODEC_ID_BINKAUDIO_DCT,
|
|
||||||
CODEC_ID_AAC_LATM,
|
|
||||||
CODEC_ID_QDMC,
|
|
||||||
CODEC_ID_CELT,
|
|
||||||
CODEC_ID_G723_1,
|
|
||||||
CODEC_ID_G729,
|
|
||||||
CODEC_ID_8SVX_EXP,
|
|
||||||
CODEC_ID_8SVX_FIB,
|
|
||||||
CODEC_ID_BMV_AUDIO,
|
|
||||||
CODEC_ID_RALF,
|
|
||||||
CODEC_ID_IAC,
|
|
||||||
CODEC_ID_ILBC,
|
|
||||||
CODEC_ID_FFWAVESYNTH = MKBETAG('F','F','W','S'),
|
|
||||||
CODEC_ID_SONIC = MKBETAG('S','O','N','C'),
|
|
||||||
CODEC_ID_SONIC_LS = MKBETAG('S','O','N','L'),
|
|
||||||
CODEC_ID_PAF_AUDIO = MKBETAG('P','A','F','A'),
|
|
||||||
CODEC_ID_OPUS = MKBETAG('O','P','U','S'),
|
|
||||||
|
|
||||||
/* subtitle codecs */
|
|
||||||
CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
|
|
||||||
CODEC_ID_DVD_SUBTITLE = 0x17000,
|
|
||||||
CODEC_ID_DVB_SUBTITLE,
|
|
||||||
CODEC_ID_TEXT, ///< raw UTF-8 text
|
|
||||||
CODEC_ID_XSUB,
|
|
||||||
CODEC_ID_SSA,
|
|
||||||
CODEC_ID_MOV_TEXT,
|
|
||||||
CODEC_ID_HDMV_PGS_SUBTITLE,
|
|
||||||
CODEC_ID_DVB_TELETEXT,
|
|
||||||
CODEC_ID_SRT,
|
|
||||||
CODEC_ID_MICRODVD = MKBETAG('m','D','V','D'),
|
|
||||||
CODEC_ID_EIA_608 = MKBETAG('c','6','0','8'),
|
|
||||||
CODEC_ID_JACOSUB = MKBETAG('J','S','U','B'),
|
|
||||||
CODEC_ID_SAMI = MKBETAG('S','A','M','I'),
|
|
||||||
CODEC_ID_REALTEXT = MKBETAG('R','T','X','T'),
|
|
||||||
CODEC_ID_SUBVIEWER = MKBETAG('S','u','b','V'),
|
|
||||||
|
|
||||||
/* other specific kind of codecs (generally used for attachments) */
|
|
||||||
CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
|
|
||||||
CODEC_ID_TTF = 0x18000,
|
|
||||||
CODEC_ID_BINTEXT = MKBETAG('B','T','X','T'),
|
|
||||||
CODEC_ID_XBIN = MKBETAG('X','B','I','N'),
|
|
||||||
CODEC_ID_IDF = MKBETAG( 0 ,'I','D','F'),
|
|
||||||
CODEC_ID_OTF = MKBETAG( 0 ,'O','T','F'),
|
|
||||||
|
|
||||||
CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
|
|
||||||
|
|
||||||
CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
|
|
||||||
* stream (only used by libavformat) */
|
|
||||||
CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
|
|
||||||
* stream (only used by libavformat) */
|
|
||||||
CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
|
|
||||||
|
|
||||||
#endif /* AVCODEC_OLD_CODEC_IDS_H */
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
/*
|
|
||||||
* Video Acceleration API (shared data between FFmpeg and the video player)
|
|
||||||
* HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
|
|
||||||
*
|
|
||||||
* Copyright (C) 2008-2009 Splitted-Desktop Systems
|
|
||||||
*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_VAAPI_H
|
|
||||||
#define AVCODEC_VAAPI_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup lavc_codec_hwaccel_vaapi
|
|
||||||
* Public libavcodec VA API header.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
|
|
||||||
* @ingroup lavc_codec_hwaccel
|
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This structure is used to share data between the FFmpeg library and
|
|
||||||
* the client video application.
|
|
||||||
* This shall be zero-allocated and available as
|
|
||||||
* AVCodecContext.hwaccel_context. All user members can be set once
|
|
||||||
* during initialization or through each AVCodecContext.get_buffer()
|
|
||||||
* function call. In any case, they must be valid prior to calling
|
|
||||||
* decoding functions.
|
|
||||||
*/
|
|
||||||
struct vaapi_context {
|
|
||||||
/**
|
|
||||||
* Window system dependent data
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by user
|
|
||||||
*/
|
|
||||||
void *display;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration ID
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by user
|
|
||||||
*/
|
|
||||||
uint32_t config_id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context ID (video decode pipeline)
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by user
|
|
||||||
*/
|
|
||||||
uint32_t context_id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VAPictureParameterBuffer ID
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
uint32_t pic_param_buf_id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VAIQMatrixBuffer ID
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
uint32_t iq_matrix_buf_id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VABitPlaneBuffer ID (for VC-1 decoding)
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
uint32_t bitplane_buf_id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Slice parameter/data buffer IDs
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
uint32_t *slice_buf_ids;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of effective slice buffer IDs to send to the HW
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
unsigned int n_slice_buf_ids;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Size of pre-allocated slice_buf_ids
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
unsigned int slice_buf_ids_alloc;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pointer to VASliceParameterBuffers
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
void *slice_params;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Size of a VASliceParameterBuffer element
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
unsigned int slice_param_size;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Size of pre-allocated slice_params
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
unsigned int slice_params_alloc;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of slices currently filled in
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
unsigned int slice_count;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pointer to slice data buffer base
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
const uint8_t *slice_data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current size of slice data
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set by libavcodec
|
|
||||||
*/
|
|
||||||
uint32_t slice_data_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* @} */
|
|
||||||
|
|
||||||
#endif /* AVCODEC_VAAPI_H */
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
/*
|
|
||||||
* VDA HW acceleration
|
|
||||||
*
|
|
||||||
* copyright (c) 2011 Sebastien Zwickert
|
|
||||||
*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_VDA_H
|
|
||||||
#define AVCODEC_VDA_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup lavc_codec_hwaccel_vda
|
|
||||||
* Public libavcodec VDA header.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
// emmintrin.h is unable to compile with -std=c99 -Werror=missing-prototypes
|
|
||||||
// http://openradar.appspot.com/8026390
|
|
||||||
#undef __GNUC_STDC_INLINE__
|
|
||||||
|
|
||||||
#define Picture QuickdrawPicture
|
|
||||||
#include <VideoDecodeAcceleration/VDADecoder.h>
|
|
||||||
#undef Picture
|
|
||||||
|
|
||||||
#include "libavcodec/version.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @defgroup lavc_codec_hwaccel_vda VDA
|
|
||||||
* @ingroup lavc_codec_hwaccel
|
|
||||||
*
|
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This structure is used to provide the necessary configurations and data
|
|
||||||
* to the VDA FFmpeg HWAccel implementation.
|
|
||||||
*
|
|
||||||
* The application must make it available as AVCodecContext.hwaccel_context.
|
|
||||||
*/
|
|
||||||
struct vda_context {
|
|
||||||
/**
|
|
||||||
* VDA decoder object.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by libavcodec.
|
|
||||||
*/
|
|
||||||
VDADecoder decoder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Core Video pixel buffer that contains the current image data.
|
|
||||||
*
|
|
||||||
* encoding: unused
|
|
||||||
* decoding: Set by libavcodec. Unset by user.
|
|
||||||
*/
|
|
||||||
CVPixelBufferRef cv_buffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the hardware decoder in synchronous mode.
|
|
||||||
*
|
|
||||||
* encoding: unused
|
|
||||||
* decoding: Set by user.
|
|
||||||
*/
|
|
||||||
int use_sync_decoding;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The frame width.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by user.
|
|
||||||
*/
|
|
||||||
int width;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The frame height.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by user.
|
|
||||||
*/
|
|
||||||
int height;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The frame format.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by user.
|
|
||||||
*/
|
|
||||||
int format;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pixel format for output image buffers.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by user.
|
|
||||||
*/
|
|
||||||
OSType cv_pix_fmt_type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current bitstream buffer.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by libavcodec.
|
|
||||||
*/
|
|
||||||
uint8_t *priv_bitstream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current size of the bitstream.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by libavcodec.
|
|
||||||
*/
|
|
||||||
int priv_bitstream_size;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The reference size used for fast reallocation.
|
|
||||||
*
|
|
||||||
* - encoding: unused
|
|
||||||
* - decoding: Set/Unset by libavcodec.
|
|
||||||
*/
|
|
||||||
int priv_allocated_size;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use av_buffer to manage buffer.
|
|
||||||
* When the flag is set, the CVPixelBuffers returned by the decoder will
|
|
||||||
* be released automatically, so you have to retain them if necessary.
|
|
||||||
* Not setting this flag may cause memory leak.
|
|
||||||
*
|
|
||||||
* encoding: unused
|
|
||||||
* decoding: Set by user.
|
|
||||||
*/
|
|
||||||
int use_ref_buffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Create the video decoder. */
|
|
||||||
int ff_vda_create_decoder(struct vda_context *vda_ctx,
|
|
||||||
uint8_t *extradata,
|
|
||||||
int extradata_size);
|
|
||||||
|
|
||||||
/** Destroy the video decoder. */
|
|
||||||
int ff_vda_destroy_decoder(struct vda_context *vda_ctx);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#endif /* AVCODEC_VDA_H */
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
/*
|
|
||||||
* The Video Decode and Presentation API for UNIX (VDPAU) is used for
|
|
||||||
* hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
|
|
||||||
*
|
|
||||||
* Copyright (C) 2008 NVIDIA
|
|
||||||
*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_VDPAU_H
|
|
||||||
#define AVCODEC_VDPAU_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup lavc_codec_hwaccel_vdpau
|
|
||||||
* Public libavcodec VDPAU header.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
|
|
||||||
* @ingroup lavc_codec_hwaccel
|
|
||||||
*
|
|
||||||
* VDPAU hardware acceleration has two modules
|
|
||||||
* - VDPAU decoding
|
|
||||||
* - VDPAU presentation
|
|
||||||
*
|
|
||||||
* The VDPAU decoding module parses all headers using FFmpeg
|
|
||||||
* parsing mechanisms and uses VDPAU for the actual decoding.
|
|
||||||
*
|
|
||||||
* As per the current implementation, the actual decoding
|
|
||||||
* and rendering (API calls) are done as part of the VDPAU
|
|
||||||
* presentation (vo_vdpau.c) module.
|
|
||||||
*
|
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <vdpau/vdpau.h>
|
|
||||||
#include <vdpau/vdpau_x11.h>
|
|
||||||
#include "libavutil/avconfig.h"
|
|
||||||
#include "libavutil/attributes.h"
|
|
||||||
|
|
||||||
#ifndef FF_API_CAP_VDPAU
|
|
||||||
#define FF_API_CAP_VDPAU 1
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_BUFS_VDPAU
|
|
||||||
#define FF_API_BUFS_VDPAU 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if FF_API_BUFS_VDPAU
|
|
||||||
union AVVDPAUPictureInfo {
|
|
||||||
VdpPictureInfoH264 h264;
|
|
||||||
VdpPictureInfoMPEG1Or2 mpeg;
|
|
||||||
VdpPictureInfoVC1 vc1;
|
|
||||||
VdpPictureInfoMPEG4Part2 mpeg4;
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct AVCodecContext;
|
|
||||||
struct AVFrame;
|
|
||||||
|
|
||||||
typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *,
|
|
||||||
const VdpPictureInfo *, uint32_t,
|
|
||||||
const VdpBitstreamBuffer *);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This structure is used to share data between the libavcodec library and
|
|
||||||
* the client video application.
|
|
||||||
* The user shall allocate the structure via the av_alloc_vdpau_hwaccel
|
|
||||||
* function and make it available as
|
|
||||||
* AVCodecContext.hwaccel_context. Members can be set by the user once
|
|
||||||
* during initialization or through each AVCodecContext.get_buffer()
|
|
||||||
* function call. In any case, they must be valid prior to calling
|
|
||||||
* decoding functions.
|
|
||||||
*/
|
|
||||||
typedef struct AVVDPAUContext {
|
|
||||||
/**
|
|
||||||
* VDPAU decoder handle
|
|
||||||
*
|
|
||||||
* Set by user.
|
|
||||||
*/
|
|
||||||
VdpDecoder decoder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VDPAU decoder render callback
|
|
||||||
*
|
|
||||||
* Set by the user.
|
|
||||||
*/
|
|
||||||
VdpDecoderRender *render;
|
|
||||||
|
|
||||||
#if FF_API_BUFS_VDPAU
|
|
||||||
/**
|
|
||||||
* VDPAU picture information
|
|
||||||
*
|
|
||||||
* Set by libavcodec.
|
|
||||||
*/
|
|
||||||
attribute_deprecated
|
|
||||||
union AVVDPAUPictureInfo info;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allocated size of the bitstream_buffers table.
|
|
||||||
*
|
|
||||||
* Set by libavcodec.
|
|
||||||
*/
|
|
||||||
attribute_deprecated
|
|
||||||
int bitstream_buffers_allocated;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Useful bitstream buffers in the bitstream buffers table.
|
|
||||||
*
|
|
||||||
* Set by libavcodec.
|
|
||||||
*/
|
|
||||||
attribute_deprecated
|
|
||||||
int bitstream_buffers_used;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table of bitstream buffers.
|
|
||||||
* The user is responsible for freeing this buffer using av_freep().
|
|
||||||
*
|
|
||||||
* Set by libavcodec.
|
|
||||||
*/
|
|
||||||
attribute_deprecated
|
|
||||||
VdpBitstreamBuffer *bitstream_buffers;
|
|
||||||
#endif
|
|
||||||
AVVDPAU_Render2 render2;
|
|
||||||
} AVVDPAUContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief allocation function for AVVDPAUContext
|
|
||||||
*
|
|
||||||
* Allows extending the struct without breaking API/ABI
|
|
||||||
*/
|
|
||||||
AVVDPAUContext *av_alloc_vdpaucontext(void);
|
|
||||||
|
|
||||||
AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
|
|
||||||
void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
|
|
||||||
|
|
||||||
#if FF_API_CAP_VDPAU
|
|
||||||
/** @brief The videoSurface is used for rendering. */
|
|
||||||
#define FF_VDPAU_STATE_USED_FOR_RENDER 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The videoSurface is needed for reference/prediction.
|
|
||||||
* The codec manipulates this.
|
|
||||||
*/
|
|
||||||
#define FF_VDPAU_STATE_USED_FOR_REFERENCE 2
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief This structure is used as a callback between the FFmpeg
|
|
||||||
* decoder (vd_) and presentation (vo_) module.
|
|
||||||
* This is used for defining a video frame containing surface,
|
|
||||||
* picture parameter, bitstream information etc which are passed
|
|
||||||
* between the FFmpeg decoder and its clients.
|
|
||||||
*/
|
|
||||||
struct vdpau_render_state {
|
|
||||||
VdpVideoSurface surface; ///< Used as rendered surface, never changed.
|
|
||||||
|
|
||||||
int state; ///< Holds FF_VDPAU_STATE_* values.
|
|
||||||
|
|
||||||
#if AV_HAVE_INCOMPATIBLE_LIBAV_ABI
|
|
||||||
/** picture parameter information for all supported codecs */
|
|
||||||
union AVVDPAUPictureInfo info;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/** Describe size/location of the compressed video data.
|
|
||||||
Set to 0 when freeing bitstream_buffers. */
|
|
||||||
int bitstream_buffers_allocated;
|
|
||||||
int bitstream_buffers_used;
|
|
||||||
/** The user is responsible for freeing this buffer using av_freep(). */
|
|
||||||
VdpBitstreamBuffer *bitstream_buffers;
|
|
||||||
|
|
||||||
#if !AV_HAVE_INCOMPATIBLE_LIBAV_ABI
|
|
||||||
/** picture parameter information for all supported codecs */
|
|
||||||
union AVVDPAUPictureInfo info;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* @}*/
|
|
||||||
|
|
||||||
#endif /* AVCODEC_VDPAU_H */
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_VERSION_H
|
|
||||||
#define AVCODEC_VERSION_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup libavc
|
|
||||||
* Libavcodec version macros.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "libavutil/avutil.h"
|
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_MAJOR 55
|
|
||||||
#define LIBAVCODEC_VERSION_MINOR 39
|
|
||||||
#define LIBAVCODEC_VERSION_MICRO 101
|
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
|
||||||
LIBAVCODEC_VERSION_MINOR, \
|
|
||||||
LIBAVCODEC_VERSION_MICRO)
|
|
||||||
#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
|
|
||||||
LIBAVCODEC_VERSION_MINOR, \
|
|
||||||
LIBAVCODEC_VERSION_MICRO)
|
|
||||||
#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
|
|
||||||
|
|
||||||
#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FF_API_* defines may be placed below to indicate public API that will be
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FF_API_REQUEST_CHANNELS
|
|
||||||
#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#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
|
|
||||||
#define FF_API_OLD_DECODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_OLD_TIMECODE
|
|
||||||
#define FF_API_OLD_TIMECODE (LIBAVCODEC_VERSION_MAJOR < 55)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef FF_API_OLD_ENCODE_AUDIO
|
|
||||||
#define FF_API_OLD_ENCODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_OLD_ENCODE_VIDEO
|
|
||||||
#define FF_API_OLD_ENCODE_VIDEO (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_CODEC_ID
|
|
||||||
#define FF_API_CODEC_ID (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_AVCODEC_RESAMPLE
|
|
||||||
#define FF_API_AVCODEC_RESAMPLE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_DEINTERLACE
|
|
||||||
#define FF_API_DEINTERLACE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_DESTRUCT_PACKET
|
|
||||||
#define FF_API_DESTRUCT_PACKET (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_GET_BUFFER
|
|
||||||
#define FF_API_GET_BUFFER (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_MISSING_SAMPLE
|
|
||||||
#define FF_API_MISSING_SAMPLE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_LOWRES
|
|
||||||
#define FF_API_LOWRES (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_CAP_VDPAU
|
|
||||||
#define FF_API_CAP_VDPAU (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_BUFS_VDPAU
|
|
||||||
#define FF_API_BUFS_VDPAU (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
#ifndef FF_API_VOXWARE
|
|
||||||
#define FF_API_VOXWARE (LIBAVCODEC_VERSION_MAJOR < 56)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* AVCODEC_VERSION_H */
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2003 Ivan Kalvachev
|
|
||||||
*
|
|
||||||
* This file is part of FFmpeg.
|
|
||||||
*
|
|
||||||
* FFmpeg is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU Lesser General Public
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 2.1 of the License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* FFmpeg is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
* Lesser General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Lesser General Public
|
|
||||||
* License along with FFmpeg; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef AVCODEC_XVMC_H
|
|
||||||
#define AVCODEC_XVMC_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file
|
|
||||||
* @ingroup lavc_codec_hwaccel_xvmc
|
|
||||||
* Public libavcodec XvMC header.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <X11/extensions/XvMC.h>
|
|
||||||
|
|
||||||
#include "avcodec.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @defgroup lavc_codec_hwaccel_xvmc XvMC
|
|
||||||
* @ingroup lavc_codec_hwaccel
|
|
||||||
*
|
|
||||||
* @{
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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) */
|
|
||||||
|
|
||||||
struct xvmc_pix_fmt {
|
|
||||||
/** The field contains the special constant value AV_XVMC_ID.
|
|
||||||
It is used as a test that the application correctly uses the API,
|
|
||||||
and that there is no corruption caused by pixel routines.
|
|
||||||
- application - set during initialization
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
int xvmc_id;
|
|
||||||
|
|
||||||
/** Pointer to the block array allocated by XvMCCreateBlocks().
|
|
||||||
The array has to be freed by XvMCDestroyBlocks().
|
|
||||||
Each group of 64 values represents one data block of differential
|
|
||||||
pixel information (in MoCo mode) or coefficients for IDCT.
|
|
||||||
- application - set the pointer during initialization
|
|
||||||
- libavcodec - fills coefficients/pixel data into the array
|
|
||||||
*/
|
|
||||||
short* data_blocks;
|
|
||||||
|
|
||||||
/** Pointer to the macroblock description array allocated by
|
|
||||||
XvMCCreateMacroBlocks() and freed by XvMCDestroyMacroBlocks().
|
|
||||||
- application - set the pointer during initialization
|
|
||||||
- libavcodec - fills description data into the array
|
|
||||||
*/
|
|
||||||
XvMCMacroBlock* mv_blocks;
|
|
||||||
|
|
||||||
/** Number of macroblock descriptions that can be stored in the mv_blocks
|
|
||||||
array.
|
|
||||||
- application - set during initialization
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
int allocated_mv_blocks;
|
|
||||||
|
|
||||||
/** Number of blocks that can be stored at once in the data_blocks array.
|
|
||||||
- application - set during initialization
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
int allocated_data_blocks;
|
|
||||||
|
|
||||||
/** Indicate that the hardware would interpret data_blocks as IDCT
|
|
||||||
coefficients and perform IDCT on them.
|
|
||||||
- application - set during initialization
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
int idct;
|
|
||||||
|
|
||||||
/** In MoCo mode it indicates that intra macroblocks are assumed to be in
|
|
||||||
unsigned format; same as the XVMC_INTRA_UNSIGNED flag.
|
|
||||||
- application - set during initialization
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
int unsigned_intra;
|
|
||||||
|
|
||||||
/** Pointer to the surface allocated by XvMCCreateSurface().
|
|
||||||
It has to be freed by XvMCDestroySurface() on application exit.
|
|
||||||
It identifies the frame and its state on the video hardware.
|
|
||||||
- application - set during initialization
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
XvMCSurface* p_surface;
|
|
||||||
|
|
||||||
/** Set by the decoder before calling ff_draw_horiz_band(),
|
|
||||||
needed by the XvMCRenderSurface function. */
|
|
||||||
//@{
|
|
||||||
/** Pointer to the surface used as past reference
|
|
||||||
- application - unchanged
|
|
||||||
- libavcodec - set
|
|
||||||
*/
|
|
||||||
XvMCSurface* p_past_surface;
|
|
||||||
|
|
||||||
/** Pointer to the surface used as future reference
|
|
||||||
- application - unchanged
|
|
||||||
- libavcodec - set
|
|
||||||
*/
|
|
||||||
XvMCSurface* p_future_surface;
|
|
||||||
|
|
||||||
/** top/bottom field or frame
|
|
||||||
- application - unchanged
|
|
||||||
- libavcodec - set
|
|
||||||
*/
|
|
||||||
unsigned int picture_structure;
|
|
||||||
|
|
||||||
/** XVMC_SECOND_FIELD - 1st or 2nd field in the sequence
|
|
||||||
- application - unchanged
|
|
||||||
- libavcodec - set
|
|
||||||
*/
|
|
||||||
unsigned int flags;
|
|
||||||
//}@
|
|
||||||
|
|
||||||
/** Number of macroblock descriptions in the mv_blocks array
|
|
||||||
that have already been passed to the hardware.
|
|
||||||
- application - zeroes it on get_buffer().
|
|
||||||
A successful ff_draw_horiz_band() may increment it
|
|
||||||
with filled_mb_block_num or zero both.
|
|
||||||
- libavcodec - unchanged
|
|
||||||
*/
|
|
||||||
int start_mv_blocks_num;
|
|
||||||
|
|
||||||
/** Number of new macroblock descriptions in the mv_blocks array (after
|
|
||||||
start_mv_blocks_num) that are filled by libavcodec and have to be
|
|
||||||
passed to the hardware.
|
|
||||||
- application - zeroes it on get_buffer() or after successful
|
|
||||||
ff_draw_horiz_band().
|
|
||||||
- libavcodec - increment with one of each stored MB
|
|
||||||
*/
|
|
||||||
int filled_mv_blocks_num;
|
|
||||||
|
|
||||||
/** Number of the next free data block; one data block consists of
|
|
||||||
64 short values in the data_blocks array.
|
|
||||||
All blocks before this one have already been claimed by placing their
|
|
||||||
position into the corresponding block description structure field,
|
|
||||||
that are part of the mv_blocks array.
|
|
||||||
- application - zeroes it on get_buffer().
|
|
||||||
A successful ff_draw_horiz_band() may zero it together
|
|
||||||
with start_mb_blocks_num.
|
|
||||||
- libavcodec - each decoded macroblock increases it by the number
|
|
||||||
of coded blocks it contains.
|
|
||||||
*/
|
|
||||||
int next_free_data_block_num;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#endif /* AVCODEC_XVMC_H */
|
|
||||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 515 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user