mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 18:40:46 +00:00
Compare commits
882 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
964d2ce59c | ||
|
|
dc52684cbc | ||
|
|
191bedc56f | ||
|
|
47b2ace7fd | ||
|
|
9fb7359a3e | ||
|
|
4a5de26406 | ||
|
|
6fa18e126f | ||
|
|
1149002e0c | ||
|
|
d704cb0b50 | ||
|
|
d59e5ae9cf | ||
|
|
4587c1550d | ||
|
|
b5bd329ada | ||
|
|
beccd7a4ac | ||
|
|
61262fa939 | ||
|
|
7c6b006631 | ||
|
|
dbd149354a | ||
|
|
4306ba5004 | ||
|
|
6de370b82f | ||
|
|
45781666b8 | ||
|
|
538231eb6f | ||
|
|
eb74f87f2c | ||
|
|
59d71ffdcf | ||
|
|
d1b93d4011 | ||
|
|
d8ddf2e740 | ||
|
|
581327dc8e | ||
|
|
76e4512a0c | ||
|
|
efdd55beca | ||
|
|
2c115649b9 | ||
|
|
2ddcc31a93 | ||
|
|
3bcce5b749 | ||
|
|
80dac27214 | ||
|
|
4a1177d048 | ||
|
|
4725d8f270 | ||
|
|
07b3528515 | ||
|
|
d2d1b1ea26 | ||
|
|
232b897abc | ||
|
|
efd076bc6c | ||
|
|
cc877480ff | ||
|
|
363145a284 | ||
|
|
755571ad33 | ||
|
|
39edb55721 | ||
|
|
15aa7ecc2e | ||
|
|
ce9e91153e | ||
|
|
9ee0a46606 | ||
|
|
20dc351f4c | ||
|
|
c30c54d562 | ||
|
|
45ff51c0d2 | ||
|
|
5b86e99138 | ||
|
|
0c72910eb7 | ||
|
|
3b0f485b41 | ||
|
|
2be2c95212 | ||
|
|
e7aeeb8bd5 | ||
|
|
73df93f86a | ||
|
|
9cd4d5e2aa | ||
|
|
c3b81554f4 | ||
|
|
6f79c52fc5 | ||
|
|
29bc3e022b | ||
|
|
7d03203d83 | ||
|
|
11dde835d1 | ||
|
|
52c47c288c | ||
|
|
63072aa8e1 | ||
|
|
4cca3ac922 | ||
|
|
604bc1ec11 | ||
|
|
5d7fbf3195 | ||
|
|
8c56e6f0d4 | ||
|
|
2069be7932 | ||
|
|
9c1c2991a9 | ||
|
|
81dabf2713 | ||
|
|
27520cb77e | ||
|
|
f555d3dae0 | ||
|
|
70f1a2cacb | ||
|
|
7f15aaa2e5 | ||
|
|
e5726205c4 | ||
|
|
07fabc0663 | ||
|
|
800f97ae85 | ||
|
|
3ee5b284e1 | ||
|
|
c0389f0da9 | ||
|
|
a7a4d7ded5 | ||
|
|
87cd974b79 | ||
|
|
7faaac31ff | ||
|
|
7386eb2a78 | ||
|
|
49a1524f4f | ||
|
|
c957b8b06b | ||
|
|
a3a6e14d80 | ||
|
|
7231f5468b | ||
|
|
4dfb0d7220 | ||
|
|
2f4f53b048 | ||
|
|
b6e8389544 | ||
|
|
d113878613 | ||
|
|
f7ed7e06db | ||
|
|
977a1d4a3c | ||
|
|
eefc08db47 | ||
|
|
ab2b1663d3 | ||
|
|
04b8a718e3 | ||
|
|
37cf260ba6 | ||
|
|
8f91fe4cd1 | ||
|
|
9246ad412f | ||
|
|
1ccbbdd4fb | ||
|
|
16cf37994d | ||
|
|
01e84624c2 | ||
|
|
939cd7cf70 | ||
|
|
4b11603035 | ||
|
|
ca18b6b052 | ||
|
|
3d0d19e561 | ||
|
|
ae463a8735 | ||
|
|
7e797829ae | ||
|
|
431ed6bc5d | ||
|
|
e9bb711c42 | ||
|
|
623bc5c156 | ||
|
|
cfefef4619 | ||
|
|
4a9a881c1f | ||
|
|
13a06d585c | ||
|
|
1c8ad64da0 | ||
|
|
1d8925de57 | ||
|
|
0eb7e779b8 | ||
|
|
a4b86eefe2 | ||
|
|
902a58bc70 | ||
|
|
a34a44f29a | ||
|
|
454fe80172 | ||
|
|
81b6a8a311 | ||
|
|
3011a5bad7 | ||
|
|
dcb7be3acd | ||
|
|
68a6b510b1 | ||
|
|
dca3e89303 | ||
|
|
bae6fef588 | ||
|
|
37f65e43a5 | ||
|
|
8c910101c7 | ||
|
|
112d9c41eb | ||
|
|
c91d1097f6 | ||
|
|
105c2c9eef | ||
|
|
b754d2de28 | ||
|
|
f6425c7ec6 | ||
|
|
e690c9b8c8 | ||
|
|
f87cbac77c | ||
|
|
150bd313cf | ||
|
|
bc90cb894c | ||
|
|
c51a75a681 | ||
|
|
68aa9bd12d | ||
|
|
1fb6bf4d70 | ||
|
|
b4df3658f1 | ||
|
|
4efb4d9b24 | ||
|
|
d61e893731 | ||
|
|
951e44728e | ||
|
|
8dcdf73222 | ||
|
|
44c3b0af57 | ||
|
|
2b295400ac | ||
|
|
aa8d8e93d2 | ||
|
|
89be7eac0e | ||
|
|
f3847b932b | ||
|
|
5e4f37532c | ||
|
|
f3f5ca74a3 | ||
|
|
e50b7076a1 | ||
|
|
36ab5aa1b6 | ||
|
|
a0a2b299d9 | ||
|
|
14d354fc29 | ||
|
|
342515f916 | ||
|
|
5f5944c237 | ||
|
|
c025432ad6 | ||
|
|
171a6437fe | ||
|
|
11b3648fac | ||
|
|
d1fae89d6d | ||
|
|
5c06848fe9 | ||
|
|
b50e506e58 | ||
|
|
59fafa163d | ||
|
|
22d84b5763 | ||
|
|
6d186892a8 | ||
|
|
88d6143897 | ||
|
|
b729fba75e | ||
|
|
c0d3f9fa48 | ||
|
|
af5e7a0e33 | ||
|
|
371d96ea65 | ||
|
|
e9e332ff85 | ||
|
|
e133ac2815 | ||
|
|
1dba5d147e | ||
|
|
1616c0b022 | ||
|
|
bcee2cf0e3 | ||
|
|
3e7ddab0e9 | ||
|
|
5da0177356 | ||
|
|
7e21638811 | ||
|
|
db5b7ab867 | ||
|
|
3bcc1c84bb | ||
|
|
d46053f8d6 | ||
|
|
00a5fed9e9 | ||
|
|
b6315a715a | ||
|
|
0da8303468 | ||
|
|
c821c4684f | ||
|
|
6bae33f822 | ||
|
|
08d4ab67a6 | ||
|
|
62203d2f21 | ||
|
|
4968dcc558 | ||
|
|
6d66d1371f | ||
|
|
b87ca71103 | ||
|
|
c251cd2e8f | ||
|
|
593616d2d9 | ||
|
|
a2fc62a4a8 | ||
|
|
fd457c5dea | ||
|
|
64e1ba500c | ||
|
|
235a0635be | ||
|
|
61f8fa7c5a | ||
|
|
7c3c107381 | ||
|
|
555477751f | ||
|
|
9364f43c52 | ||
|
|
0be3169c2c | ||
|
|
5199d90505 | ||
|
|
5b5277bf3f | ||
|
|
ad3614c58e | ||
|
|
9401ecc9fb | ||
|
|
1711e5e1a4 | ||
|
|
f28f9bc65f | ||
|
|
8eb4014f01 | ||
|
|
df0d7952db | ||
|
|
77d1770063 | ||
|
|
f433bfdc02 | ||
|
|
f75b6f9b80 | ||
|
|
621df9996d | ||
|
|
6c29503db9 | ||
|
|
304a02e2ec | ||
|
|
7aea7ed8c6 | ||
|
|
e5ab3baa7b | ||
|
|
41b73f7cd9 | ||
|
|
38da42caf3 | ||
|
|
424d71fa13 | ||
|
|
dbc9d78002 | ||
|
|
b7ef8f54b7 | ||
|
|
bea7cab0c3 | ||
|
|
352b6f7dd9 | ||
|
|
8665fe364f | ||
|
|
7d023c8865 | ||
|
|
503d4b970c | ||
|
|
6b07072a08 | ||
|
|
2b02af5d98 | ||
|
|
613c068523 | ||
|
|
0b0181f35c | ||
|
|
c873bae3e4 | ||
|
|
7397a97a9e | ||
|
|
b567db9ab7 | ||
|
|
3440f54598 | ||
|
|
d533b25b29 | ||
|
|
72290bd725 | ||
|
|
fdd4c0bbe1 | ||
|
|
0ac83e1cf7 | ||
|
|
e27129fc48 | ||
|
|
d54fdc9f5f | ||
|
|
dc984e8679 | ||
|
|
ee46906376 | ||
|
|
1d76536e31 | ||
|
|
dc97adc7a1 | ||
|
|
a1c659b7b8 | ||
|
|
9997f164b9 | ||
|
|
2f7ac67cb0 | ||
|
|
27f0fd63b3 | ||
|
|
9f56fdfbb9 | ||
|
|
83b66b19de | ||
|
|
9a4f85e752 | ||
|
|
d00824d49c | ||
|
|
ba0171221c | ||
|
|
68040394fb | ||
|
|
6fa1c35521 | ||
|
|
7a3fbd8dae | ||
|
|
329ee1a0bc | ||
|
|
11908e07bf | ||
|
|
fd53122cb3 | ||
|
|
d9c0830198 | ||
|
|
d0aafb3814 | ||
|
|
40a3cc2ecb | ||
|
|
4469013bb5 | ||
|
|
78393932d0 | ||
|
|
dbc2491151 | ||
|
|
936834e396 | ||
|
|
01eb7a2b64 | ||
|
|
252285e4f7 | ||
|
|
df7333b8d0 | ||
|
|
cf98ec2c41 | ||
|
|
0afda10bcb | ||
|
|
754773420f | ||
|
|
71aadfa2f5 | ||
|
|
f7bfa63145 | ||
|
|
6574a0aab2 | ||
|
|
5d4988969e | ||
|
|
5121eb1852 | ||
|
|
004aeef2a7 | ||
|
|
aa65a0312a | ||
|
|
a1b58ab2fc | ||
|
|
d32c0e32d0 | ||
|
|
9cd71e2855 | ||
|
|
1308a4ed80 | ||
|
|
deb78e1c64 | ||
|
|
9aec6b1d31 | ||
|
|
97702b8861 | ||
|
|
832e7197c5 | ||
|
|
26b992726c | ||
|
|
1cb3588841 | ||
|
|
b461d546d6 | ||
|
|
b7810d6eb6 | ||
|
|
6fb3a8e57d | ||
|
|
b521c784bc | ||
|
|
8e1641af5f | ||
|
|
c0aac01d33 | ||
|
|
4f8b0adcbb | ||
|
|
d7bdfb4db9 | ||
|
|
393a4c9c8a | ||
|
|
99b53f9a6a | ||
|
|
8da563b280 | ||
|
|
5cca5cd352 | ||
|
|
d48e964d05 | ||
|
|
ad94978f98 | ||
|
|
d5b950e5cf | ||
|
|
c46b9acf6b | ||
|
|
1d65baa981 | ||
|
|
d8e322bac9 | ||
|
|
44871626cf | ||
|
|
f661522b5d | ||
|
|
8b7287a5d6 | ||
|
|
a454b0ab78 | ||
|
|
c4ae049091 | ||
|
|
75bf84d0d9 | ||
|
|
c248994ed4 | ||
|
|
0e4e3d80d2 | ||
|
|
59b2449cdc | ||
|
|
a7a34ec629 | ||
|
|
8d469c5d0a | ||
|
|
e6979d50b5 | ||
|
|
640255efb2 | ||
|
|
6e25b135a3 | ||
|
|
04e093a2c2 | ||
|
|
1bffa6bf41 | ||
|
|
69b175573d | ||
|
|
0eaff9d328 | ||
|
|
44905ed774 | ||
|
|
813f2edd95 | ||
|
|
337d753a33 | ||
|
|
1137c74f76 | ||
|
|
0c1451f757 | ||
|
|
5ab9ea48fd | ||
|
|
ffcb623040 | ||
|
|
bfe6929642 | ||
|
|
50d45011a8 | ||
|
|
2f7087d6d3 | ||
|
|
92b71588d0 | ||
|
|
4f3d018764 | ||
|
|
a22e33eeb9 | ||
|
|
10e0a262f7 | ||
|
|
6a939e7495 | ||
|
|
422e703c2f | ||
|
|
f8ba7cf190 | ||
|
|
27191da45e | ||
|
|
d1e135db4d | ||
|
|
61a17afe69 | ||
|
|
47fd691884 | ||
|
|
0d171c6b28 | ||
|
|
f0c69d08b8 | ||
|
|
629bf5766d | ||
|
|
233bceeece | ||
|
|
24926c75f1 | ||
|
|
6660ea7d91 | ||
|
|
4864b2ca45 | ||
|
|
92097b318d | ||
|
|
997898c99d | ||
|
|
1174e03885 | ||
|
|
ff0f54d541 | ||
|
|
a83f6f2ef7 | ||
|
|
814964a100 | ||
|
|
7e154292a9 | ||
|
|
0220dd921a | ||
|
|
0f9cba1053 | ||
|
|
05e4792d6f | ||
|
|
c9b5c00756 | ||
|
|
a4e134589d | ||
|
|
cd80a94f28 | ||
|
|
57c645a291 | ||
|
|
6f35b991b7 | ||
|
|
0cba200207 | ||
|
|
81582d7343 | ||
|
|
04e561fd54 | ||
|
|
a7023f52aa | ||
|
|
5efbb5229d | ||
|
|
541e43eb18 | ||
|
|
7e679ff4c6 | ||
|
|
486b4b4c4c | ||
|
|
752b204be8 | ||
|
|
7d76bf7868 | ||
|
|
564e7c71a6 | ||
|
|
db49077b9b | ||
|
|
67f01fbdca | ||
|
|
16b845ab84 | ||
|
|
5c175fecf6 | ||
|
|
773976b265 | ||
|
|
80070bbdbe | ||
|
|
4c8d433b6c | ||
|
|
404f096d11 | ||
|
|
d2ac927cec | ||
|
|
5e3d59d3d7 | ||
|
|
9cd2ce1309 | ||
|
|
9ed49730d4 | ||
|
|
39ebb48f58 | ||
|
|
1c29c70fba | ||
|
|
6993051529 | ||
|
|
4930087c4d | ||
|
|
795f0a013b | ||
|
|
213414778e | ||
|
|
7eac0ccaf8 | ||
|
|
02b74fbbc5 | ||
|
|
6adc9dcb2d | ||
|
|
be620908f9 | ||
|
|
efcfcf88db | ||
|
|
e4edfdb043 | ||
|
|
3b5028d1a4 | ||
|
|
bc8c45bd59 | ||
|
|
63eb346a70 | ||
|
|
68028242b4 | ||
|
|
27ad691d23 | ||
|
|
747e920061 | ||
|
|
bae3b4a6e8 | ||
|
|
8d09f56a0e | ||
|
|
113a0e2c45 | ||
|
|
977215a098 | ||
|
|
a7e65b47f9 | ||
|
|
7126055ad6 | ||
|
|
3de9765eaa | ||
|
|
d4072eb295 | ||
|
|
cac2bdbb81 | ||
|
|
66f0aee3f8 | ||
|
|
b690dc5474 | ||
|
|
514e0ca2c9 | ||
|
|
c9cf485025 | ||
|
|
c2fbe6ad91 | ||
|
|
cf07c02398 | ||
|
|
42dc928ad5 | ||
|
|
11597f0aa7 | ||
|
|
cdcd4d48f2 | ||
|
|
a9af4e54a9 | ||
|
|
7eac609219 | ||
|
|
fa761debc4 | ||
|
|
62e175f069 | ||
|
|
d7d8c40565 | ||
|
|
64de13ab50 | ||
|
|
2f02939638 | ||
|
|
1d7c8697e9 | ||
|
|
16c4b2532d | ||
|
|
7dea322bbd | ||
|
|
a64db9d86f | ||
|
|
349ecb16ab | ||
|
|
a3867735c1 | ||
|
|
5b087e9f70 | ||
|
|
14d9f77e4e | ||
|
|
eed18223eb | ||
|
|
30d4d2a918 | ||
|
|
40c3db0214 | ||
|
|
30f666c70e | ||
|
|
209fead0e8 | ||
|
|
5c6889bf6d | ||
|
|
7d24900756 | ||
|
|
f7c5039912 | ||
|
|
79a75b9d19 | ||
|
|
3c37c89db8 | ||
|
|
29b64992bd | ||
|
|
c9b14540f2 | ||
|
|
6995a27126 | ||
|
|
546843a26c | ||
|
|
d03d260535 | ||
|
|
6946e3c7a2 | ||
|
|
b79d328961 | ||
|
|
c313797d93 | ||
|
|
c8cb8e1346 | ||
|
|
f61540c099 | ||
|
|
6a9f8da14e | ||
|
|
6a6bd9fb0b | ||
|
|
ff9260a0fd | ||
|
|
62bedb1609 | ||
|
|
a519723d44 | ||
|
|
c2a16a9b4a | ||
|
|
b64c84a5c3 | ||
|
|
42a502eff1 | ||
|
|
46d72b912c | ||
|
|
722f397819 | ||
|
|
8f169b976b | ||
|
|
53452d22c0 | ||
|
|
9d4e9631bc | ||
|
|
5942545b9c | ||
|
|
035a62856d | ||
|
|
636c1ceb26 | ||
|
|
37cde22a55 | ||
|
|
772835689d | ||
|
|
7dcc689014 | ||
|
|
90981a6643 | ||
|
|
de05a5b446 | ||
|
|
f644436aeb | ||
|
|
7a8166ec09 | ||
|
|
6eed8408fc | ||
|
|
4b9ee92434 | ||
|
|
756ceaff1a | ||
|
|
d3be670974 | ||
|
|
7b41b1158e | ||
|
|
d0da5d3702 | ||
|
|
42668b5699 | ||
|
|
9444430830 | ||
|
|
e30088e53b | ||
|
|
a989bdde80 | ||
|
|
e5d9da447c | ||
|
|
ba5c026bff | ||
|
|
66536aa755 | ||
|
|
a53444148e | ||
|
|
d237ceb1df | ||
|
|
1f504288cb | ||
|
|
0543420624 | ||
|
|
6580eb8ea4 | ||
|
|
8584bf1910 | ||
|
|
e7f92d3667 | ||
|
|
fe71b1be20 | ||
|
|
0ba0d37a37 | ||
|
|
673a115b52 | ||
|
|
bedf472e9e | ||
|
|
f4abc66eeb | ||
|
|
164e6f83d8 | ||
|
|
0074848a4e | ||
|
|
8c09154183 | ||
|
|
c8339d5eae | ||
|
|
07e4991c56 | ||
|
|
4eb62e6c5f | ||
|
|
bf82556783 | ||
|
|
f282e84174 | ||
|
|
ed1a56dc68 | ||
|
|
af04831fb0 | ||
|
|
8f3eecd980 | ||
|
|
636c20d67b | ||
|
|
5d90950591 | ||
|
|
7651ce5e84 | ||
|
|
b3a1938c1d | ||
|
|
0ce1e1be27 | ||
|
|
470680d463 | ||
|
|
63e2fd447d | ||
|
|
864bcadcb2 | ||
|
|
ae852eb911 | ||
|
|
203fcd82e7 | ||
|
|
67469103d4 | ||
|
|
9e413000a5 | ||
|
|
bedcbfbb7e | ||
|
|
73e4970a43 | ||
|
|
ac8b7ae960 | ||
|
|
c62986e7b1 | ||
|
|
a3d5e955aa | ||
|
|
04e77e557b | ||
|
|
a748b54041 | ||
|
|
f77673a5c8 | ||
|
|
23ebc4d927 | ||
|
|
92b86674b9 | ||
|
|
f94d224395 | ||
|
|
822f498646 | ||
|
|
4566c1855b | ||
|
|
195bf8ed55 | ||
|
|
52678cfe35 | ||
|
|
61f89a2d4c | ||
|
|
ba398e4073 | ||
|
|
a14a4a8d60 | ||
|
|
9caf3b37ac | ||
|
|
e6965605c9 | ||
|
|
5b355a3e73 | ||
|
|
92d534a9c3 | ||
|
|
2d08f568e9 | ||
|
|
a9f7b1aeab | ||
|
|
4f53cfcb20 | ||
|
|
ffd70986b3 | ||
|
|
b5e89e47b6 | ||
|
|
439afd15fa | ||
|
|
8d2bfecb10 | ||
|
|
7d15c34ed2 | ||
|
|
4ef1b8dc4c | ||
|
|
c0d64058fd | ||
|
|
3c11ff63a7 | ||
|
|
537a50bee5 | ||
|
|
1a58b228a0 | ||
|
|
248a135f86 | ||
|
|
246fb69050 | ||
|
|
a907dd0084 | ||
|
|
fe58361724 | ||
|
|
a0f93a2dc3 | ||
|
|
82390ec9b9 | ||
|
|
34ef95926e | ||
|
|
faa0cba39d | ||
|
|
8b395bb29f | ||
|
|
31d7f237eb | ||
|
|
ef8c49f135 | ||
|
|
a8c460e715 | ||
|
|
d75e42e23d | ||
|
|
b191425112 | ||
|
|
c06a4ab76d | ||
|
|
b3042312f6 | ||
|
|
5fd105c9a9 | ||
|
|
306c2d143b | ||
|
|
a71a3e22e6 | ||
|
|
da4fab2f3e | ||
|
|
1db84efb68 | ||
|
|
c1c3af3c66 | ||
|
|
4622b9f202 | ||
|
|
47ea158c4c | ||
|
|
d9cb5eacf8 | ||
|
|
5718c47be7 | ||
|
|
d7d90e8e49 | ||
|
|
4d420b29cb | ||
|
|
0b9e7aa05b | ||
|
|
8c663cc84a | ||
|
|
8d1417c636 | ||
|
|
b91ab53219 | ||
|
|
6eeb7ae5b2 | ||
|
|
0436179020 | ||
|
|
c92cae51c8 | ||
|
|
57da68c0e2 | ||
|
|
67dc2ef9ab | ||
|
|
6459579f15 | ||
|
|
5112179fca | ||
|
|
b640564689 | ||
|
|
763f8938b3 | ||
|
|
4c67631ea5 | ||
|
|
920154b4b6 | ||
|
|
d8c7d10ed6 | ||
|
|
adcffa62d8 | ||
|
|
260d716eb8 | ||
|
|
fc1c26b5d7 | ||
|
|
df59c99f80 | ||
|
|
8a465edad9 | ||
|
|
9b9020b512 | ||
|
|
b9ac48532f | ||
|
|
886ef425e6 | ||
|
|
fbd61d2a21 | ||
|
|
469dcab5c7 | ||
|
|
fd1cb52f5f | ||
|
|
5175e68b99 | ||
|
|
64aa01b2cf | ||
|
|
63964ba6a7 | ||
|
|
e82683b0f4 | ||
|
|
4b2299ed02 | ||
|
|
4a4f89a992 | ||
|
|
d3a7bba666 | ||
|
|
8bd6582d07 | ||
|
|
875089305b | ||
|
|
c19ff71c9a | ||
|
|
36c320a584 | ||
|
|
316b8c56f1 | ||
|
|
2cf3855d35 | ||
|
|
13ec16c606 | ||
|
|
7d150e7e89 | ||
|
|
7d61948d91 | ||
|
|
73de3cc91d | ||
|
|
d71cbe344a | ||
|
|
dbe01a17d2 | ||
|
|
b3503cdede | ||
|
|
1ac6439690 | ||
|
|
c481841ddf | ||
|
|
678269c561 | ||
|
|
83e874cdb6 | ||
|
|
90fc5797d5 | ||
|
|
fcfcce88dd | ||
|
|
fb8fc54bb1 | ||
|
|
a4ec619e5a | ||
|
|
8fb2622b66 | ||
|
|
e6527de786 | ||
|
|
3f7e4d817f | ||
|
|
6eaabc84aa | ||
|
|
814557435d | ||
|
|
2d5328fc24 | ||
|
|
95c82c5cc5 | ||
|
|
fe907b0271 | ||
|
|
7ae74a6a18 | ||
|
|
9772049295 | ||
|
|
ec39f22ad8 | ||
|
|
51343a171d | ||
|
|
9988330613 | ||
|
|
8235717502 | ||
|
|
3d29d76cd4 | ||
|
|
cde5367f38 | ||
|
|
466463ebc3 | ||
|
|
aee255a6ee | ||
|
|
daf7598774 | ||
|
|
6de5cf8925 | ||
|
|
52e5817327 | ||
|
|
79ddfc65d5 | ||
|
|
6acb1fd92a | ||
|
|
bd2a1b8886 | ||
|
|
021cfd1737 | ||
|
|
5fc438a0be | ||
|
|
3a3ac83ab5 | ||
|
|
ff4570abac | ||
|
|
c819f2f0e3 | ||
|
|
c2cdb1264d | ||
|
|
c0e95ea18b | ||
|
|
f21a81d7ac | ||
|
|
67c726a141 | ||
|
|
ff5f50e3ec | ||
|
|
022a08f5a1 | ||
|
|
52f81274f0 | ||
|
|
35a50209be | ||
|
|
9728c136f5 | ||
|
|
bacd1d81fd | ||
|
|
f36227b506 | ||
|
|
debd840db4 | ||
|
|
1531629fcd | ||
|
|
7bc325fa08 | ||
|
|
2cc2d05c2f | ||
|
|
1df03e137b | ||
|
|
b0169b0edf | ||
|
|
ae79f03e61 | ||
|
|
77a587abe8 | ||
|
|
7f587dc389 | ||
|
|
8dbb03114d | ||
|
|
415e96dec6 | ||
|
|
abc7f135f3 | ||
|
|
4b93207def | ||
|
|
f004ae6a41 | ||
|
|
ac6120adc4 | ||
|
|
d9c2d58519 | ||
|
|
22865b8af4 | ||
|
|
431ba06742 | ||
|
|
ed8857552b | ||
|
|
5f42ca66fe | ||
|
|
c9d003ca6d | ||
|
|
330f40cc18 | ||
|
|
44366db4d5 | ||
|
|
93bf28b87d | ||
|
|
bd9c6834b7 | ||
|
|
96ad2bcdef | ||
|
|
14c03f0b37 | ||
|
|
5bd30fe3dc | ||
|
|
5be499887d | ||
|
|
7124963c56 | ||
|
|
4377808896 | ||
|
|
50f8f78b8d | ||
|
|
1bb9a13c17 | ||
|
|
e7f1b822f7 | ||
|
|
c9eee2e075 | ||
|
|
6556b3eb9b | ||
|
|
cc92f3829e | ||
|
|
33ffbe151f | ||
|
|
60db0ff775 | ||
|
|
7ecac185ac | ||
|
|
b76495fa8f | ||
|
|
463d4ad3fd | ||
|
|
0f0e41d5a4 | ||
|
|
2d55562dd3 | ||
|
|
cbe40fde92 | ||
|
|
97e62fdd34 | ||
|
|
aa799342e5 | ||
|
|
ae8cb18f63 | ||
|
|
8f53b6f233 | ||
|
|
9e385215ce | ||
|
|
378fbedfa4 | ||
|
|
c84e063114 | ||
|
|
8d316ce9f0 | ||
|
|
0019b93e8d | ||
|
|
2d66c6fb53 | ||
|
|
b629f674ca | ||
|
|
670622dfd7 | ||
|
|
f35c2ead0f | ||
|
|
6b7b797089 | ||
|
|
15d4f6354d | ||
|
|
77cea99b35 | ||
|
|
70b50bd096 | ||
|
|
c2401e7a75 | ||
|
|
b63c6223b0 | ||
|
|
bcf10cc0b2 | ||
|
|
aaa73eb196 | ||
|
|
707c7a1a53 | ||
|
|
dcb3e1c0e4 | ||
|
|
894110ba08 | ||
|
|
a4dceb0b74 | ||
|
|
1c82fdf048 | ||
|
|
ec303e485f | ||
|
|
1cdcc6d190 | ||
|
|
ef1f44f873 | ||
|
|
00e81e87de | ||
|
|
2f082b9f85 | ||
|
|
c9e5230e37 | ||
|
|
86e2657613 | ||
|
|
c23470af40 | ||
|
|
6c5ec3d2e9 | ||
|
|
38423a9f37 | ||
|
|
7b10e52808 | ||
|
|
890ee846f7 | ||
|
|
6a92ea74fc | ||
|
|
07cf96c5ce | ||
|
|
636c5f17f5 | ||
|
|
4a2ee91700 | ||
|
|
8e9d605248 | ||
|
|
a4e6738353 | ||
|
|
0f815a0085 | ||
|
|
0c8c108bd1 | ||
|
|
04941212cd | ||
|
|
9ad45a3ca3 | ||
|
|
176c8e9b93 | ||
|
|
f537588228 | ||
|
|
e593c04001 | ||
|
|
1095d7808c | ||
|
|
d29dccba69 | ||
|
|
a4098919b9 | ||
|
|
09e7ff0582 | ||
|
|
09177be8f7 | ||
|
|
eaa08bada4 | ||
|
|
5f93d55dab | ||
|
|
dfc0d518f8 | ||
|
|
eace3a0bf0 | ||
|
|
92adbe0983 | ||
|
|
aadbc3dd01 | ||
|
|
d01a28c57f | ||
|
|
4ee99a78b2 | ||
|
|
bd9b37a5a0 | ||
|
|
7947d8b75d | ||
|
|
3408e467d5 | ||
|
|
a0237a19d9 | ||
|
|
eb15599c01 | ||
|
|
8c9d0d171c | ||
|
|
a39f4c5eab | ||
|
|
239dffcbdf | ||
|
|
3af3df0544 | ||
|
|
7e30d043eb | ||
|
|
4cbaee6806 | ||
|
|
8297ca7e85 | ||
|
|
da1c350067 | ||
|
|
c8c7512600 | ||
|
|
9b6e12497e | ||
|
|
50e7deeb32 | ||
|
|
7e3acd0213 | ||
|
|
e60420cb2c | ||
|
|
c733be5611 | ||
|
|
4fbe93e62d | ||
|
|
bc2ca0b386 | ||
|
|
ccc3eeebe8 | ||
|
|
63ee6ef79a | ||
|
|
cf3ac50d22 | ||
|
|
cdf634dc41 | ||
|
|
21116f90a7 | ||
|
|
29dd0e172c | ||
|
|
2d5083179c | ||
|
|
a96de39b28 | ||
|
|
c93812179f | ||
|
|
26809c4b6b | ||
|
|
0cce5b021e | ||
|
|
d54c1b07ce | ||
|
|
1a38cc2c0c | ||
|
|
e188e1dd04 | ||
|
|
ae18e00b13 | ||
|
|
c18b6ec00b | ||
|
|
53c7bb0338 | ||
|
|
dfa3be78e4 | ||
|
|
932bb1145b | ||
|
|
46f4f5ccbe | ||
|
|
7f851c46f4 | ||
|
|
82ae042f1c | ||
|
|
8ca3aab363 | ||
|
|
f95cd60cfd | ||
|
|
ab1e47edb4 | ||
|
|
6a695d2c72 | ||
|
|
421d73b28a | ||
|
|
042f67506c | ||
|
|
96e5513cdb | ||
|
|
cc30752eb7 | ||
|
|
ade061bf3c | ||
|
|
616945a963 | ||
|
|
3201fac36c | ||
|
|
84551df36a | ||
|
|
b32899f101 | ||
|
|
b4a0f81eda | ||
|
|
95d035f00b | ||
|
|
666fbecc01 | ||
|
|
6f8306cc18 | ||
|
|
b0d5b9c767 | ||
|
|
3648c0f26a | ||
|
|
f1b4fdd8b0 | ||
|
|
12658f4fb0 | ||
|
|
ed92f9d28e | ||
|
|
a1440621f9 | ||
|
|
48f8a05bae | ||
|
|
4701c22b67 | ||
|
|
7f841c1fca | ||
|
|
4f56dce9f7 | ||
|
|
4e9fb1bbce | ||
|
|
da47b43ad3 | ||
|
|
87152e6403 | ||
|
|
895c123b13 | ||
|
|
35476e2c28 | ||
|
|
29909e07e8 | ||
|
|
9ac103187f | ||
|
|
ce1494895e | ||
|
|
41d2f6b0e2 |
4
.github/auto-comment.yml
vendored
Normal file
4
.github/auto-comment.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
issuesOpened: >
|
||||
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||
Thank you, and happy streaming!
|
||||
8
.github/no-response.yml
vendored
Normal file
8
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||
|
||||
daysUntilClose: 7
|
||||
responseRequiredLabel: 'need more info'
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there was no response to a
|
||||
request for more information from the issue opener. Please leave a comment or
|
||||
open a new issue if you have additional information related to this issue.
|
||||
14
.github/stale.yml
vendored
Normal file
14
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||
|
||||
daysUntilStale: 90
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- accepted
|
||||
- bug
|
||||
- enhancement
|
||||
- meta
|
||||
staleLabel: stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
closeComment: false
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,3 +38,6 @@ build/
|
||||
# Compiled JNI libraries folder
|
||||
**/jniLibs
|
||||
app/.externalNativeBuild/
|
||||
|
||||
# NDK stuff
|
||||
.cxx/
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "moonlight-common"]
|
||||
path = moonlight-common
|
||||
url = https://github.com/moonlight-stream/moonlight-common.git
|
||||
[submodule "app/src/main/jni/moonlight-core/moonlight-common-c"]
|
||||
path = app/src/main/jni/moonlight-core/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
|
||||
18
.travis.yml
Normal file
18
.travis.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
language: android
|
||||
dist: trusty
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-30.0.0
|
||||
- android-30
|
||||
|
||||
before_install:
|
||||
- sdkmanager --list
|
||||
|
||||
install:
|
||||
- yes | sdkmanager "ndk;21.0.6113669"
|
||||
55
README.md
55
README.md
@@ -1,56 +1,27 @@
|
||||
# Moonlight
|
||||
# Moonlight Android
|
||||
|
||||
[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.
|
||||
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
||||
|
||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||
[Moonlight for Android](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
|
||||
Moonlight for Android 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.
|
||||
|
||||
[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.
|
||||
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [iOS/tvOS client](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||
Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed project information, setup guide, or troubleshooting steps.
|
||||
|
||||
## Features
|
||||
|
||||
* Streams any of your games from your PC to your Android device
|
||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||
* Automatically finds GameStream-compatible PCs on your network
|
||||
|
||||
## Installation
|
||||
|
||||
* Download and install Moonlight for Android from
|
||||
[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
|
||||
|
||||
## Requirements
|
||||
|
||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
||||
* Android device running 4.1 (Jelly Bean) or higher
|
||||
* High-end wireless router (802.11n dual-band recommended)
|
||||
|
||||
## Usage
|
||||
|
||||
* Turn on GameStream in the GFE settings
|
||||
* If you are connecting from outside the same network, turn on internet
|
||||
streaming
|
||||
* 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 and add the PIN if needed
|
||||
* Tap your PC again to view the list of apps to stream
|
||||
* Play games!
|
||||
|
||||
## Contribute
|
||||
|
||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||
|
||||
1. Fork us
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
## Downloads
|
||||
* [Google Play Store](https://play.google.com/store/apps/details?id=com.limelight)
|
||||
* [Amazon App Store](https://www.amazon.com/gp/product/B00JK4MFN2)
|
||||
* [F-Droid](https://f-droid.org/packages/com.limelight)
|
||||
* [APK](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||
|
||||
## 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
|
||||
* Build the APK using Android Studio or gradle
|
||||
|
||||
## Authors
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
buildToolsVersion '28.0.0'
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 30
|
||||
|
||||
versionName "5.8.2"
|
||||
versionCode = 167
|
||||
versionName "9.6"
|
||||
versionCode = 231
|
||||
}
|
||||
|
||||
flavorDimensions "root"
|
||||
@@ -44,6 +43,7 @@ android {
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
|
||||
bundle {
|
||||
@@ -114,9 +114,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.64'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.64'
|
||||
implementation 'org.jcodec:jcodec:0.2.3'
|
||||
|
||||
implementation project(':moonlight-common')
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||
implementation 'com.squareup.okio:okio:1.17.5'
|
||||
implementation 'org.jmdns:jmdns:3.5.5'
|
||||
}
|
||||
|
||||
6
app/lint.xml
Normal file
6
app/lint.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="InvalidPackage">
|
||||
<ignore path="**/bcpkix-jdk15on-*.jar"/>
|
||||
</issue>
|
||||
</lint>
|
||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -25,3 +25,4 @@
|
||||
|
||||
# jMDNS
|
||||
-dontwarn javax.jmdns.impl.DNSCache
|
||||
-dontwarn org.slf4j.**
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
@@ -38,8 +41,14 @@
|
||||
android:appCategory="game"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:installLocation="auto"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<provider
|
||||
android:name=".PosterContentProvider"
|
||||
android:authorities="poster.${applicationId}"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
</provider>
|
||||
<!-- Samsung multi-window support -->
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
@@ -67,8 +76,9 @@
|
||||
</activity>
|
||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||
<activity
|
||||
android:name=".AppViewShortcutTrampoline"
|
||||
android:name=".ShortcutTrampoline"
|
||||
android:noHistory="true"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<meta-data
|
||||
@@ -94,6 +104,7 @@
|
||||
<activity
|
||||
android:name=".preferences.AddComputerManually"
|
||||
android:resizeableActivity="true"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:label="Add Computer Manually">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
@@ -109,7 +120,8 @@
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTask"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/StreamTheme">
|
||||
android:theme="@style/StreamTheme"
|
||||
android:preferMinimalPostProcessing="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.AppView" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
@@ -26,6 +26,10 @@ import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.ContextMenu;
|
||||
@@ -36,10 +40,13 @@ import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private AppGridAdapter appGridAdapter;
|
||||
private String uuidString;
|
||||
@@ -56,10 +63,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private final static int START_OR_RESUME_ID = 1;
|
||||
private final static int QUIT_ID = 2;
|
||||
private final static int CANCEL_ID = 3;
|
||||
private final static int START_WTIH_QUIT = 4;
|
||||
private final static int START_WITH_QUIT = 4;
|
||||
private final static int VIEW_DETAILS_ID = 5;
|
||||
private final static int CREATE_SHORTCUT_ID = 6;
|
||||
|
||||
public final static String NAME_EXTRA = "Name";
|
||||
public final static String UUID_EXTRA = "UUID";
|
||||
public final static String NEW_PAIR_EXTRA = "NewPair";
|
||||
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@@ -74,27 +84,32 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
// 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));
|
||||
computer = localBinder.getComputer(uuidString);
|
||||
if (computer == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(computer, true, getIntent().getBooleanExtra(NEW_PAIR_EXTRA, false));
|
||||
shortcutHelper.reportComputerShortcutUsed(computer);
|
||||
|
||||
try {
|
||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||
computer, managerBinder.getUniqueId());
|
||||
PreferenceConfiguration.readPreferences(AppView.this),
|
||||
computer, localBinder.getUniqueId());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Now make the binder visible. We must do this after appGridAdapter
|
||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||
// touching the appGridAdapter prior to initialization.
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Load the app grid with cached data (if possible).
|
||||
// This must be done _before_ startComputerUpdates()
|
||||
// so the initial serverinfo response can update the running
|
||||
@@ -132,6 +147,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// If appGridAdapter is initialized, let it know about the configuration change.
|
||||
// If not, it will pick it up when it initializes.
|
||||
if (appGridAdapter != null) {
|
||||
// Update the app grid adapter to create grid items with the correct layout
|
||||
appGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
|
||||
|
||||
try {
|
||||
// Reinflate the app grid itself to pick up the layout change
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startComputerUpdates() {
|
||||
// Don't start polling if we're not bound or in the foreground
|
||||
if (managerBinder == null || !inForeground) {
|
||||
@@ -147,7 +183,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,7 +207,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
// Disable shortcuts referencing this PC for now
|
||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||
shortcutHelper.disableComputerShortcut(details,
|
||||
getResources().getString(R.string.scut_not_paired));
|
||||
|
||||
// Display a toast to the user and quit the activity
|
||||
@@ -207,7 +243,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -251,14 +289,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||
|
||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||
TextView label = findViewById(R.id.appListText);
|
||||
setTitle(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);
|
||||
setTitle(computerName);
|
||||
label.setText(computerName);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
@@ -272,7 +305,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||
updateUiWithAppList(applist);
|
||||
LimeLog.info("Loaded applist from cache");
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
if (lastRawApplist != null) {
|
||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||
e.printStackTrace();
|
||||
@@ -331,10 +364,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||
}
|
||||
else {
|
||||
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||
}
|
||||
}
|
||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Only add an option to create shortcut if box art is loaded
|
||||
// and when we're in grid-mode (not list-mode).
|
||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||
if (appImageView != null) {
|
||||
// We have a grid ImageView, so we must be in grid-mode
|
||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
||||
if (drawable != null && drawable.getBitmap() != null) {
|
||||
// We have a bitmap loaded too
|
||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -346,7 +394,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||
switch (item.getItemId()) {
|
||||
case START_WTIH_QUIT:
|
||||
case START_WITH_QUIT:
|
||||
// Display a confirmation dialog first
|
||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||
@Override
|
||||
@@ -367,8 +415,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
suspendGridUpdates = true;
|
||||
ServerHelper.doQuit(AppView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
ServerHelper.doQuit(AppView.this, computer,
|
||||
app.app, managerBinder, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -386,6 +433,18 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
case CANCEL_ID:
|
||||
return true;
|
||||
|
||||
case VIEW_DETAILS_ID:
|
||||
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), app.app.toString(), false);
|
||||
return true;
|
||||
|
||||
case CREATE_SHORTCUT_ID:
|
||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
|
||||
if (!shortcutHelper.createPinnedGameShortcut(computer, app.app, appBits)) {
|
||||
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
@@ -457,6 +516,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
if (!foundExistingApp) {
|
||||
// This app must be new
|
||||
appGridAdapter.addApp(new AppObject(app));
|
||||
|
||||
// We could have a leftover shortcut from last time this PC was paired
|
||||
// or if this app was removed then added again. Enable those shortcuts
|
||||
// again if present.
|
||||
shortcutHelper.enableAppShortcut(computer, app);
|
||||
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
@@ -477,6 +542,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
// This app was removed in the latest app list
|
||||
if (!foundExistingApp) {
|
||||
shortcutHelper.disableAppShortcut(computer, existingApp.app, "App removed from PC");
|
||||
appGridAdapter.removeApp(existingApp);
|
||||
updated = true;
|
||||
|
||||
@@ -498,9 +564,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
@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);
|
||||
return PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||
R.layout.app_grid_view_small : R.layout.app_grid_view;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -520,6 +585,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
}
|
||||
});
|
||||
UiHelper.applyStatusBarPadding(listView);
|
||||
registerForContextMenu(listView);
|
||||
listView.requestFocus();
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
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 the managerBinder was destroyed before this callback,
|
||||
// just finish the activity.
|
||||
if (managerBinder == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
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, false), 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();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
25
app/src/main/java/com/limelight/LimeLog.java
Normal file
25
app/src/main/java/com/limelight/LimeLog.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.FileHandler;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class LimeLog {
|
||||
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||
|
||||
public static void info(String msg) {
|
||||
LOGGER.info(msg);
|
||||
}
|
||||
|
||||
public static void warning(String msg) {
|
||||
LOGGER.warning(msg);
|
||||
}
|
||||
|
||||
public static void severe(String msg) {
|
||||
LOGGER.severe(msg);
|
||||
}
|
||||
|
||||
public static void setFileHandler(String fileName) throws IOException {
|
||||
LOGGER.addHandler(new FileHandler(fileName));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.grid.PcGridAdapter;
|
||||
import com.limelight.grid.assets.DiskAssetLoader;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
@@ -28,6 +29,7 @@ import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
@@ -52,6 +54,8 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
@@ -112,6 +116,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
private final static int DELETE_ID = 5;
|
||||
private final static int RESUME_ID = 6;
|
||||
private final static int QUIT_ID = 7;
|
||||
private final static int VIEW_DETAILS_ID = 8;
|
||||
|
||||
private void initializeViews() {
|
||||
setContentView(R.layout.activity_pc_view);
|
||||
@@ -121,6 +126,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Set default preferences if we've never been run
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||
|
||||
// Set the correct layout for the PC grid
|
||||
pcGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
|
||||
|
||||
// Setup the list view
|
||||
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
||||
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
||||
@@ -218,9 +226,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
|
||||
pcGridAdapter = new PcGridAdapter(this,
|
||||
PreferenceConfiguration.readPreferences(this).listMode,
|
||||
PreferenceConfiguration.readPreferences(this).smallIconMode);
|
||||
pcGridAdapter = new PcGridAdapter(this, PreferenceConfiguration.readPreferences(this));
|
||||
|
||||
initializeViews();
|
||||
}
|
||||
@@ -311,8 +317,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||
|
||||
// Inflate the context menu
|
||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||
computer.details.state == ComputerDetails.State.UNKNOWN) {
|
||||
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));
|
||||
}
|
||||
@@ -332,6 +338,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// it with delete which actually work
|
||||
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||
}
|
||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -343,7 +350,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doPair(final ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -369,9 +377,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
if (httpConn.getPairState() == PairState.PAIRED) {
|
||||
// Don't display any toast, but open the app list
|
||||
message = null;
|
||||
success = true;
|
||||
@@ -383,21 +391,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
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(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
PairingManager pm = httpConn.getPairingManager();
|
||||
|
||||
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.FAILED) {
|
||||
else if (pairState == PairState.FAILED) {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
||||
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||
message = getResources().getString(R.string.pair_already_in_progress);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||
else if (pairState == PairState.PAIRED) {
|
||||
// Just navigate to the app view without displaying a toast
|
||||
message = null;
|
||||
success = true;
|
||||
|
||||
// Pin this certificate for later HTTPS use
|
||||
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
||||
|
||||
// Invalidate reachability information after pairing to force
|
||||
// a refresh before reading pair state again
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
@@ -411,7 +424,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
message = e.getMessage();
|
||||
}
|
||||
@@ -429,7 +442,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
if (toastSuccess) {
|
||||
// Open the app list after a successful pairing attempt
|
||||
doAppList(computer);
|
||||
doAppList(computer, true);
|
||||
}
|
||||
else {
|
||||
// Start polling again if we're still in the foreground
|
||||
@@ -475,7 +488,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doUnpair(final ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -493,7 +507,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
try {
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
httpConn.unpair();
|
||||
@@ -511,8 +525,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
message = e.getMessage();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
final String toastMessage = message;
|
||||
@@ -526,8 +541,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void doAppList(ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -538,7 +553,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
Intent i = new Intent(this, AppView.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
@@ -560,16 +576,24 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
return true;
|
||||
|
||||
case DELETE_ID:
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
if (ActivityManager.isUserAMonkey()) {
|
||||
LimeLog.info("Ignoring delete PC request from monkey");
|
||||
return true;
|
||||
}
|
||||
managerBinder.removeComputer(computer.details.name);
|
||||
removeComputer(computer.details);
|
||||
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
removeComputer(computer.details);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case APP_LIST_ID:
|
||||
doAppList(computer.details);
|
||||
doAppList(computer.details, false);
|
||||
return true;
|
||||
|
||||
case RESUME_ID:
|
||||
@@ -591,25 +615,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ServerHelper.doQuit(PcView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||
ServerHelper.doQuit(PcView.this, computer.details,
|
||||
new NvApp("app", 0, false), managerBinder, null);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case VIEW_DETAILS_ID:
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeComputer(ComputerDetails details) {
|
||||
managerBinder.removeComputer(details);
|
||||
|
||||
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
||||
|
||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||
|
||||
if (details.equals(computer.details)) {
|
||||
// Disable or delete shortcuts referencing this PC
|
||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||
shortcutHelper.disableComputerShortcut(details,
|
||||
getResources().getString(R.string.scut_deleted_pc));
|
||||
|
||||
pcGridAdapter.removeComputer(computer);
|
||||
@@ -640,7 +671,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
// Add a launcher shortcut for this PC
|
||||
if (details.pairState == PairState.PAIRED) {
|
||||
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
||||
shortcutHelper.createAppViewShortcutForOnlineHost(details);
|
||||
}
|
||||
|
||||
if (existingEntry != null) {
|
||||
@@ -661,9 +692,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
@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);
|
||||
return R.layout.pc_grid_view;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -674,18 +703,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
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) {
|
||||
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
|
||||
computer.details.state == ComputerDetails.State.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);
|
||||
doAppList(computer.details, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
UiHelper.applyStatusBarPadding(listView);
|
||||
registerForContextMenu(listView);
|
||||
}
|
||||
|
||||
|
||||
107
app/src/main/java/com/limelight/PosterContentProvider.java
Normal file
107
app/src/main/java/com/limelight/PosterContentProvider.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package com.limelight;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import com.limelight.grid.assets.DiskAssetLoader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.List;
|
||||
|
||||
public class PosterContentProvider extends ContentProvider {
|
||||
|
||||
|
||||
public static final String AUTHORITY = "poster." + BuildConfig.APPLICATION_ID;
|
||||
public static final String PNG_MIME_TYPE = "image/png";
|
||||
public static final int APP_ID_PATH_INDEX = 2;
|
||||
public static final int COMPUTER_UUID_PATH_INDEX = 1;
|
||||
private DiskAssetLoader mDiskAssetLoader;
|
||||
|
||||
private static final UriMatcher sUriMatcher;
|
||||
private static final String BOXART_PATH = "boxart";
|
||||
private static final int BOXART_URI_ID = 1;
|
||||
|
||||
static {
|
||||
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
sUriMatcher.addURI(AUTHORITY, BOXART_PATH, BOXART_URI_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
int match = sUriMatcher.match(uri);
|
||||
if (match == BOXART_URI_ID) {
|
||||
return openBoxArtFile(uri, mode);
|
||||
}
|
||||
return openBoxArtFile(uri, mode);
|
||||
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor openBoxArtFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
if (!"r".equals(mode)) {
|
||||
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||
}
|
||||
|
||||
List<String> segments = uri.getPathSegments();
|
||||
if (segments.size() != 3) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
String appId = segments.get(APP_ID_PATH_INDEX);
|
||||
String uuid = segments.get(COMPUTER_UUID_PATH_INDEX);
|
||||
File file = mDiskAssetLoader.getFile(uuid, Integer.parseInt(appId));
|
||||
if (file.exists()) {
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return PNG_MIME_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mDiskAssetLoader = new DiskAssetLoader(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
throw new UnsupportedOperationException("This provider doesn't support query");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("This provider is support read only");
|
||||
}
|
||||
|
||||
|
||||
public static Uri createBoxArtUri(String uuid, String appId) {
|
||||
return new Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath(BOXART_PATH)
|
||||
.appendPath(uuid)
|
||||
.appendPath(appId)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
277
app/src/main/java/com/limelight/ShortcutTrampoline.java
Normal file
277
app/src/main/java/com/limelight/ShortcutTrampoline.java
Normal file
@@ -0,0 +1,277 @@
|
||||
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.nvstream.http.PairingManager;
|
||||
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 ShortcutTrampoline extends Activity {
|
||||
private String uuidString;
|
||||
private NvApp app;
|
||||
private ArrayList<Intent> intentStack = new ArrayList<>();
|
||||
|
||||
private ComputerDetails computer;
|
||||
private SpinnerDialog blockingLoadSpinner;
|
||||
|
||||
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(uuidString);
|
||||
|
||||
if (computer == null) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_pc_not_found),
|
||||
true);
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
if (managerBinder != null) {
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.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 the managerBinder was destroyed before this callback,
|
||||
// just finish the activity.
|
||||
if (managerBinder == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
|
||||
|
||||
// Launch game if provided app ID, otherwise launch app view
|
||||
if (app != null) {
|
||||
if (details.runningGameId == 0 || details.runningGameId == app.getAppId()) {
|
||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder));
|
||||
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
} else {
|
||||
// Create the start intent immediately, so we can safely unbind the managerBinder
|
||||
// below before we return.
|
||||
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder);
|
||||
|
||||
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
intentStack.add(startIntent);
|
||||
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
}
|
||||
}, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Close this activity
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Add the PC view at the back (and clear the task)
|
||||
Intent i;
|
||||
i = new Intent(ShortcutTrampoline.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(ShortcutTrampoline.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(ShortcutTrampoline.this,
|
||||
new NvApp(null, details.runningGameId, false), 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(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.error_pc_offline),
|
||||
true);
|
||||
} else if (details.pairState != PairingManager.PairState.PAIRED) {
|
||||
// Computer not paired - display an error dialog
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_not_paired),
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
protected boolean validateInput(String uuidString, String appIdString) {
|
||||
// Validate UUID
|
||||
if (uuidString == null) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_invalid_uuid),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
UUID.fromString(uuidString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_invalid_uuid),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate App ID (if provided)
|
||||
if (appIdString != null && !appIdString.isEmpty()) {
|
||||
try {
|
||||
Integer.parseInt(appIdString);
|
||||
} catch (NumberFormatException ex) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_invalid_app_id),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
|
||||
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
||||
|
||||
if (validateInput(uuidString, appIdString)) {
|
||||
if (appIdString != null && !appIdString.isEmpty()) {
|
||||
app = new NvApp(getIntent().getStringExtra(Game.EXTRA_APP_NAME),
|
||||
Integer.parseInt(appIdString),
|
||||
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
|
||||
}
|
||||
|
||||
// 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 onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
Dialog.closeDialogs();
|
||||
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
||||
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
48000,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
@@ -28,7 +28,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
.setUsage(AudioAttributes.USAGE_GAME);
|
||||
AudioFormat format = new AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(48000)
|
||||
.setSampleRate(sampleRate)
|
||||
.setChannelMask(channelConfig)
|
||||
.build();
|
||||
|
||||
@@ -64,25 +64,46 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int setup(int audioConfiguration) {
|
||||
public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||
int channelConfig;
|
||||
int bytesPerFrame;
|
||||
|
||||
switch (audioConfiguration)
|
||||
switch (audioConfiguration.channelCount)
|
||||
{
|
||||
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
bytesPerFrame = 2 * 240 * 2;
|
||||
break;
|
||||
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
||||
case 4:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||
break;
|
||||
case 6:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
bytesPerFrame = 6 * 240 * 2;
|
||||
break;
|
||||
case 8:
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// AudioFormat.CHANNEL_OUT_7POINT1_SURROUND isn't available until Android 6.0,
|
||||
// yet the CHANNEL_OUT_SIDE_LEFT and CHANNEL_OUT_SIDE_RIGHT constants were added
|
||||
// in 5.0, so just hardcode the constant so we can work on Lollipop.
|
||||
channelConfig = 0x000018fc; // AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||
}
|
||||
else {
|
||||
// On KitKat and lower, creation of the AudioTrack will fail if we specify
|
||||
// CHANNEL_OUT_SIDE_LEFT or CHANNEL_OUT_SIDE_RIGHT. That leaves us with
|
||||
// the old CHANNEL_OUT_7POINT1 which uses left-of-center and right-of-center
|
||||
// speakers instead of side-left and side-right. This non-standard layout
|
||||
// is probably not what the user wants, but we don't really have a choice.
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LimeLog.severe("Decoder returned unhandled channel count");
|
||||
return -1;
|
||||
}
|
||||
|
||||
LimeLog.info("Audio channel config: "+String.format("0x%X", channelConfig));
|
||||
|
||||
bytesPerFrame = audioConfiguration.channelCount * samplesPerFrame * 2;
|
||||
|
||||
// We're not supposed to request less than the minimum
|
||||
// buffer size for our buffer, but it appears that we can
|
||||
// do this on many devices and it lowers audio latency.
|
||||
@@ -122,7 +143,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
case 1:
|
||||
case 3:
|
||||
// Try the larger buffer size
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT),
|
||||
bytesPerFrame * 2);
|
||||
@@ -135,13 +156,13 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||
// Skip low latency options if hardware sample rate doesn't match the content
|
||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
||||
track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
|
||||
track.play();
|
||||
|
||||
// Successfully created working AudioTrack. We're done here.
|
||||
@@ -168,8 +189,17 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(byte[] audioData) {
|
||||
track.write(audioData, 0, audioData.length);
|
||||
public void playDecodedAudio(short[] audioData) {
|
||||
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||
if (MoonBridge.getPendingAudioDuration() < 40) {
|
||||
// This will block until the write is completed. That can cause a backlog
|
||||
// of pending audio data, so we do the above check to be able to bound
|
||||
// latency at 40 ms in that situation.
|
||||
track.write(audioData, 0, audioData.length);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
@@ -149,13 +148,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
try {
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.media.AudioAttributes;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.SparseArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
@@ -14,6 +18,7 @@ import android.view.MotionEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.input.driver.AbstractController;
|
||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||
import com.limelight.binding.input.driver.UsbDriverService;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
@@ -47,24 +52,26 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
|
||||
|
||||
private final NvConnection conn;
|
||||
private final Context activityContext;
|
||||
private final Activity activityContext;
|
||||
private final double stickDeadzone;
|
||||
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
||||
private final GameGestures gestures;
|
||||
private final Vibrator deviceVibrator;
|
||||
private boolean hasGameController;
|
||||
|
||||
private final PreferenceConfiguration prefConfig;
|
||||
private short currentControllers, initialControllers;
|
||||
|
||||
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
|
||||
public ControllerHandler(Activity activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
|
||||
this.activityContext = activityContext;
|
||||
this.conn = conn;
|
||||
this.gestures = gestures;
|
||||
this.prefConfig = prefConfig;
|
||||
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
|
||||
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
|
||||
// HACK: For now we're hardcoding a 7% deadzone. Some deadzone
|
||||
// is required for controller batching support to work.
|
||||
int deadzonePercentage = 10;
|
||||
int deadzonePercentage = 7;
|
||||
|
||||
int[] ids = InputDevice.getDeviceIds();
|
||||
for (int id : ids) {
|
||||
@@ -102,6 +109,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
defaultContext.controllerNumber = (short) 0;
|
||||
defaultContext.assignedControllerNumber = true;
|
||||
defaultContext.external = false;
|
||||
|
||||
// Some devices (GPD XD) have a back button which sends input events
|
||||
// with device ID == 0. This hits the default context which would normally
|
||||
@@ -140,6 +148,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
if (context != null) {
|
||||
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
|
||||
releaseControllerNumber(context);
|
||||
context.destroy();
|
||||
inputDeviceContexts.remove(deviceId);
|
||||
}
|
||||
}
|
||||
@@ -151,6 +160,20 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
onInputDeviceAdded(deviceId);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||
deviceContext.destroy();
|
||||
}
|
||||
|
||||
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
|
||||
deviceContext.destroy();
|
||||
}
|
||||
|
||||
deviceVibrator.cancel();
|
||||
}
|
||||
|
||||
private static boolean hasJoystickAxes(InputDevice device) {
|
||||
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
|
||||
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
|
||||
@@ -197,15 +220,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
|
||||
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
|
||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||
// We explicitly ask not to claim devices that appear as InputDevices
|
||||
// We explicitly check not to claim devices that appear as InputDevices
|
||||
// otherwise we will double count them.
|
||||
if (UsbDriverService.shouldClaimDevice(dev, false)) {
|
||||
if (UsbDriverService.shouldClaimDevice(dev, false) &&
|
||||
!UsbDriverService.isRecognizedInputDevice(dev)) {
|
||||
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
|
||||
mask |= 1 << count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
|
||||
LimeLog.info("Counting OSC gamepad");
|
||||
mask |= 1;
|
||||
}
|
||||
|
||||
LimeLog.info("Enumerated "+count+" gamepads");
|
||||
return mask;
|
||||
}
|
||||
@@ -240,9 +269,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
InputDeviceContext devContext = (InputDeviceContext) context;
|
||||
|
||||
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
||||
if (devContext.name != null &&
|
||||
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
|
||||
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
|
||||
if (!devContext.external) {
|
||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||
context.controllerNumber = 0;
|
||||
}
|
||||
@@ -298,10 +325,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.assignedControllerNumber = true;
|
||||
}
|
||||
|
||||
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
|
||||
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
|
||||
UsbDeviceContext context = new UsbDeviceContext();
|
||||
|
||||
context.id = deviceId;
|
||||
context.id = device.getControllerId();
|
||||
context.device = device;
|
||||
context.external = true;
|
||||
|
||||
context.vendorId = device.getVendorId();
|
||||
context.productId = device.getProductId();
|
||||
|
||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||
@@ -311,17 +343,41 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
private static boolean isExternal(InputDevice dev) {
|
||||
try {
|
||||
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
|
||||
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ClassCastException e) {
|
||||
e.printStackTrace();
|
||||
// The ASUS Tinker Board inaccurately reports Bluetooth gamepads as internal,
|
||||
// causing shouldIgnoreBack() to believe it should pass through back as a
|
||||
// navigation event for any attached gamepads.
|
||||
if (Build.MODEL.equals("Tinker Board")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String deviceName = dev.getName();
|
||||
if (deviceName.contains("gpio") || // This is the back button on Shield portable consoles
|
||||
deviceName.contains("joy_key") || // These are the gamepad buttons on the Archos Gamepad 2
|
||||
deviceName.contains("keypad") || // These are gamepad buttons on the XPERIA Play
|
||||
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.01") || // Gamepad on Shield Portable
|
||||
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.02")) // Gamepad on Shield Portable (?)
|
||||
{
|
||||
LimeLog.info(dev.getName()+" is internal by hardcoded mapping");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
|
||||
return dev.isExternal();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
|
||||
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ClassCastException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Answer true if we don't know
|
||||
@@ -338,9 +394,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
// Classify this device as a remote by name if it has no joystick axes
|
||||
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
|
||||
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
|
||||
devName.toLowerCase().contains("remote")) {
|
||||
if (!hasJoystickAxes(dev) && devName.toLowerCase().contains("remote")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -363,11 +417,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
// Note that we are explicitly NOT excluding the current device we're examining here,
|
||||
// since the other gamepad buttons may be on our current device and that's fine.
|
||||
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A);
|
||||
if (keys[0]) {
|
||||
if (currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT)[0]) {
|
||||
foundInternalSelect = true;
|
||||
}
|
||||
if (keys[1]) {
|
||||
|
||||
// We don't check KEYCODE_BUTTON_A here, since the Shield Android TV has a
|
||||
// virtual mouse device that claims to have KEYCODE_BUTTON_A. Instead, we rely
|
||||
// on the SOURCE_GAMEPAD flag to be set on gamepad devices.
|
||||
if (hasGamepadButtons(currentDev)) {
|
||||
foundInternalGamepad = true;
|
||||
}
|
||||
}
|
||||
@@ -379,8 +436,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
|
||||
return !foundInternalGamepad || foundInternalSelect;
|
||||
}
|
||||
|
||||
return false;
|
||||
else {
|
||||
// For external devices, we want to pass through the back button if the device
|
||||
// has no gamepad axes or gamepad buttons.
|
||||
return !hasJoystickAxes(dev) && !hasGamepadButtons(dev);
|
||||
}
|
||||
}
|
||||
|
||||
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
||||
@@ -388,10 +448,24 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
String devName = dev.getName();
|
||||
|
||||
LimeLog.info("Creating controller context for device: "+devName);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
LimeLog.info("Vendor ID: "+dev.getVendorId());
|
||||
LimeLog.info("Product ID: "+dev.getProductId());
|
||||
}
|
||||
LimeLog.info(dev.toString());
|
||||
|
||||
context.name = devName;
|
||||
context.id = dev.getId();
|
||||
context.external = isExternal(dev);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
context.vendorId = dev.getVendorId();
|
||||
context.productId = dev.getProductId();
|
||||
}
|
||||
|
||||
if (dev.getVibrator().hasVibrator()) {
|
||||
context.vibrator = dev.getVibrator();
|
||||
}
|
||||
|
||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
@@ -430,25 +504,45 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||
if (rxRange != null && ryRange != null && devName != null) {
|
||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||
// Xbox controllers use RX and RY for right stick
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (dev.getVendorId() == 0x054c) { // Sony
|
||||
if (dev.hasKeys(KeyEvent.KEYCODE_BUTTON_C)[0]) {
|
||||
LimeLog.info("Detected non-standard DualShock 4 mapping");
|
||||
context.isNonStandardDualShock4 = true;
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Detected DualShock 4 (Linux standard mapping)");
|
||||
context.usesLinuxGamepadStandardFaceButtons = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!devName.contains("Xbox") && !devName.contains("XBox") && !devName.contains("X-Box")) {
|
||||
LimeLog.info("Assuming non-standard DualShock 4 mapping on < 4.4");
|
||||
context.isNonStandardDualShock4 = true;
|
||||
}
|
||||
|
||||
if (context.isNonStandardDualShock4) {
|
||||
// The old DS4 driver uses RX and RY for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
}
|
||||
else {
|
||||
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
|
||||
// other sane controller that uses RX and RY for right stick and Z and RZ for triggers.
|
||||
context.rightStickXAxis = MotionEvent.AXIS_RX;
|
||||
context.rightStickYAxis = MotionEvent.AXIS_RY;
|
||||
|
||||
// Xbox controllers use Z and RZ for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
context.triggersIdleNegative = true;
|
||||
context.isXboxController = true;
|
||||
// While it's likely that Z and RZ are triggers, we may have digital trigger buttons
|
||||
// instead. We must check that we actually have Z and RZ axes before assigning them.
|
||||
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z) != null &&
|
||||
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ) != null) {
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// DS4 controller uses RX and RY for triggers
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_RX;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_RY;
|
||||
context.triggersIdleNegative = true;
|
||||
|
||||
context.isDualShock4 = true;
|
||||
}
|
||||
// Triggers always idle negative on axes that are centered at zero
|
||||
context.triggersIdleNegative = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +599,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
// The device name provided is just "Gamepad" which is pretty useless, so we
|
||||
// use VID/PID instead
|
||||
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
|
||||
@@ -524,7 +618,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
if (devName.contains("ASUS Gamepad")) {
|
||||
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
|
||||
// is Android 5.0 anyway
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
|
||||
if (!hasStartKey[0] && !hasStartKey[1]) {
|
||||
context.backIsStart = true;
|
||||
@@ -554,7 +648,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
// required fixup is ignoring the select button.
|
||||
else if (devName.equals("Xbox Wireless Controller")) {
|
||||
if (gasRange == null) {
|
||||
context.isXboxBtController = true;
|
||||
context.isNonStandardXboxBtController = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -614,7 +708,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
private short getActiveControllerMask() {
|
||||
if (prefConfig.multiController) {
|
||||
return (short)(currentControllers | initialControllers);
|
||||
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
|
||||
}
|
||||
else {
|
||||
// Only Player 1 is active with multi-controller disabled
|
||||
@@ -733,7 +827,58 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isDualShock4) {
|
||||
// Override mode button for 8BitDo controllers
|
||||
if (context.vendorId == 0x2dc8 && event.getScanCode() == 306) {
|
||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||
}
|
||||
|
||||
if ((context.vendorId == 0x057e && context.productId == 0x2009) || // Switch Pro controller
|
||||
(context.vendorId == 0x0f0d && context.productId == 0x00c1)) { // HORIPAD for Switch
|
||||
switch (event.getScanCode()) {
|
||||
case 0x130:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
case 0x131:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
case 0x132:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
case 0x133:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
case 0x134:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
case 0x135:
|
||||
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||
case 0x136:
|
||||
return KeyEvent.KEYCODE_BUTTON_L2;
|
||||
case 0x137:
|
||||
return KeyEvent.KEYCODE_BUTTON_R2;
|
||||
case 0x138:
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
case 0x139:
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
case 0x13A:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||
case 0x13B:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||
case 0x13D:
|
||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.usesLinuxGamepadStandardFaceButtons) {
|
||||
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
|
||||
switch (event.getScanCode()) {
|
||||
case 304:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
case 305:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
case 307:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
case 308:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isNonStandardDualShock4) {
|
||||
switch (event.getScanCode()) {
|
||||
case 304:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
@@ -778,7 +923,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
}
|
||||
}
|
||||
else if (context.isXboxBtController) {
|
||||
else if (context.isNonStandardXboxBtController) {
|
||||
switch (event.getScanCode()) {
|
||||
case 306:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
@@ -807,19 +952,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||
}
|
||||
}
|
||||
else if (context.vendorId == 0x0b05 && // ASUS
|
||||
(context.productId == 0x7900 || // Kunai - USB
|
||||
context.productId == 0x7902)) // Kunai - Bluetooth
|
||||
{
|
||||
// ROG Kunai has special M1-M4 buttons that are accessible via the
|
||||
// joycon-style detachable controllers that we should map to Start
|
||||
// and Select.
|
||||
switch (event.getScanCode()) {
|
||||
case 264:
|
||||
case 266:
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
|
||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
||||
switch (event.getKeyCode()) {
|
||||
// These are duplicate dpad events for hat input
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
case KeyEvent.KEYCODE_DPAD_CENTER:
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
return 0;
|
||||
case 265:
|
||||
case 267:
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
}
|
||||
}
|
||||
else if (context.hatXAxis == -1 &&
|
||||
|
||||
if (context.hatXAxis == -1 &&
|
||||
context.hatYAxis == -1 &&
|
||||
/* FIXME: There's no good way to know for sure if xpad is bound
|
||||
to this device, so we won't use the name to validate if these
|
||||
@@ -877,6 +1028,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return keyCode;
|
||||
}
|
||||
|
||||
private int handleFlipFaceButtons(int keyCode) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
default:
|
||||
return keyCode;
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2d populateCachedVector(float x, float y) {
|
||||
// Reinitialize our cached Vector2d object
|
||||
inputVector.initialize(x, y);
|
||||
@@ -950,17 +1116,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
|
||||
if (hatX < -0.5) {
|
||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
context.hatXAxisUsed = true;
|
||||
}
|
||||
else if (hatX > 0.5) {
|
||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
context.hatXAxisUsed = true;
|
||||
}
|
||||
|
||||
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
|
||||
if (hatY < -0.5) {
|
||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
||||
context.hatYAxisUsed = true;
|
||||
}
|
||||
else if (hatY > 0.5) {
|
||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
context.hatYAxisUsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,6 +1213,94 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
}
|
||||
|
||||
private void rumbleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
|
||||
// Since we can only use a single amplitude value, compute the desired amplitude
|
||||
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
|
||||
// NB: This value is now 0-255 as required by VibrationEffect.
|
||||
short lowFreqMotorMSB = (short)((lowFreqMotor >> 8) & 0xFF);
|
||||
short highFreqMotorMSB = (short)((highFreqMotor >> 8) & 0xFF);
|
||||
int simulatedAmplitude = Math.min(255, (int)((lowFreqMotorMSB * 0.80) + (highFreqMotorMSB * 0.33)));
|
||||
|
||||
if (simulatedAmplitude == 0) {
|
||||
// This case is easy - just cancel the current effect and get out.
|
||||
// NB: We cannot simply check lowFreqMotor == highFreqMotor == 0
|
||||
// because our simulatedAmplitude could be 0 even though our inputs
|
||||
// are not (ex: lowFreqMotor == 0 && highFreqMotor == 1).
|
||||
vibrator.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to use amplitude-based control if we're on Oreo and the device
|
||||
// supports amplitude-based vibration control.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (vibrator.hasAmplitudeControl()) {
|
||||
VibrationEffect effect = VibrationEffect.createOneShot(60000, simulatedAmplitude);
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.build();
|
||||
vibrator.vibrate(effect, audioAttributes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this point, we don't have amplitude controls available, so
|
||||
// we must emulate it by PWMing the vibration. Ick.
|
||||
long pwmPeriod = 20;
|
||||
long onTime = (long)((simulatedAmplitude / 255.0) * pwmPeriod);
|
||||
long offTime = pwmPeriod - onTime;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.build();
|
||||
vibrator.vibrate(new long[]{0, onTime, offTime}, 0, audioAttributes);
|
||||
}
|
||||
else {
|
||||
vibrator.vibrate(new long[]{0, onTime, offTime}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||
boolean foundMatchingDevice = false;
|
||||
boolean vibrated = false;
|
||||
|
||||
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||
|
||||
if (deviceContext.controllerNumber == controllerNumber) {
|
||||
foundMatchingDevice = true;
|
||||
|
||||
if (deviceContext.vibrator != null) {
|
||||
vibrated = true;
|
||||
rumbleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
|
||||
|
||||
if (deviceContext.controllerNumber == controllerNumber) {
|
||||
foundMatchingDevice = vibrated = true;
|
||||
deviceContext.device.rumble((short)lowFreqMotor, (short)highFreqMotor);
|
||||
}
|
||||
}
|
||||
|
||||
// We may decide to rumble the device for player 1
|
||||
if (controllerNumber == 0) {
|
||||
// If we didn't find a matching device, it must be the on-screen
|
||||
// controls that triggered the rumble. Vibrate the device if
|
||||
// the user has requested that behavior.
|
||||
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
|
||||
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
|
||||
// We found a device to vibrate but it didn't have rumble support. The user
|
||||
// has requested us to vibrate the device in this case.
|
||||
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handleButtonUp(KeyEvent event) {
|
||||
InputDeviceContext context = getContextForEvent(event);
|
||||
if (context == null) {
|
||||
@@ -1050,6 +1308,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
|
||||
if (prefConfig.flipFaceButtons) {
|
||||
keyCode = handleFlipFaceButtons(keyCode);
|
||||
}
|
||||
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -1087,15 +1350,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.inputMap &= ~ControllerPacket.BACK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
if (context.hatXAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
if (context.hatXAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
if (context.hatYAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
if (context.hatYAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
@@ -1179,6 +1458,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
sendControllerInputPacket(context);
|
||||
|
||||
if (context.pendingExit && context.inputMap == 0) {
|
||||
// All buttons from the quit combo are lifted. Finish the activity now.
|
||||
activityContext.finish();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1189,6 +1474,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
|
||||
if (prefConfig.flipFaceButtons) {
|
||||
keyCode = handleFlipFaceButtons(keyCode);
|
||||
}
|
||||
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -1209,15 +1499,31 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.inputMap |= ControllerPacket.BACK_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
if (context.hatXAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
if (context.hatXAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
if (context.hatYAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap |= ControllerPacket.UP_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
if (context.hatYAxisUsed) {
|
||||
// Suppress this duplicate event if we have a hat
|
||||
return true;
|
||||
}
|
||||
context.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
@@ -1263,9 +1569,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start+Back+LB+RB is the quit combo
|
||||
if (context.inputMap == (ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG |
|
||||
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG)) {
|
||||
// Wait for the combo to lift and then finish the activity
|
||||
context.pendingExit = true;
|
||||
}
|
||||
|
||||
// Start+LB acts like select for controllers with one button
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
|
||||
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
|
||||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
||||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||
{
|
||||
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
|
||||
@@ -1275,10 +1588,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
// We detect select+start or start+RB as the special button combo
|
||||
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
|
||||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
|
||||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
|
||||
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
|
||||
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG) ||
|
||||
context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
|
||||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
|
||||
SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
|
||||
{
|
||||
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
|
||||
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
|
||||
@@ -1286,7 +1599,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
}
|
||||
|
||||
|
||||
// We don't need to send repeat key down events, but the platform
|
||||
// sends us events that claim to be repeats but they're from different
|
||||
// devices, so we just send them all and deal with some duplicates.
|
||||
@@ -1294,12 +1606,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return true;
|
||||
}
|
||||
|
||||
public void reportOscState(short buttonFlags,
|
||||
short leftStickX, short leftStickY,
|
||||
short rightStickX, short rightStickY,
|
||||
byte leftTrigger, byte rightTrigger) {
|
||||
defaultContext.leftStickX = leftStickX;
|
||||
defaultContext.leftStickY = leftStickY;
|
||||
|
||||
defaultContext.rightStickX = rightStickX;
|
||||
defaultContext.rightStickY = rightStickY;
|
||||
|
||||
defaultContext.leftTrigger = leftTrigger;
|
||||
defaultContext.rightTrigger = rightTrigger;
|
||||
|
||||
defaultContext.inputMap = buttonFlags;
|
||||
|
||||
sendControllerInputPacket(defaultContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportControllerState(int controllerId, short buttonFlags,
|
||||
float leftStickX, float leftStickY,
|
||||
float rightStickX, float rightStickY,
|
||||
float leftTrigger, float rightTrigger) {
|
||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
||||
GenericControllerContext context = usbDeviceContexts.get(controllerId);
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1334,23 +1664,28 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceRemoved(int controllerId) {
|
||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
||||
public void deviceRemoved(AbstractController controller) {
|
||||
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
|
||||
if (context != null) {
|
||||
LimeLog.info("Removed controller: "+controllerId);
|
||||
LimeLog.info("Removed controller: "+controller.getControllerId());
|
||||
releaseControllerNumber(context);
|
||||
usbDeviceContexts.remove(controllerId);
|
||||
context.destroy();
|
||||
usbDeviceContexts.remove(controller.getControllerId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceAdded(int controllerId) {
|
||||
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
|
||||
usbDeviceContexts.put(controllerId, context);
|
||||
public void deviceAdded(AbstractController controller) {
|
||||
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
|
||||
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||
}
|
||||
|
||||
class GenericControllerContext {
|
||||
public int id;
|
||||
public boolean external;
|
||||
|
||||
public int vendorId;
|
||||
public int productId;
|
||||
|
||||
public float leftStickDeadzoneRadius;
|
||||
public float rightStickDeadzoneRadius;
|
||||
@@ -1371,10 +1706,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
public boolean mouseEmulationActive;
|
||||
public Timer mouseEmulationTimer;
|
||||
public short mouseEmulationLastInputMap;
|
||||
|
||||
public void destroy() {
|
||||
if (mouseEmulationTimer != null) {
|
||||
mouseEmulationTimer.cancel();
|
||||
mouseEmulationTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InputDeviceContext extends GenericControllerContext {
|
||||
public String name;
|
||||
public Vibrator vibrator;
|
||||
|
||||
public int leftStickXAxis = -1;
|
||||
public int leftStickYAxis = -1;
|
||||
@@ -1389,15 +1732,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
|
||||
public int hatXAxis = -1;
|
||||
public int hatYAxis = -1;
|
||||
public boolean hatXAxisUsed, hatYAxisUsed;
|
||||
|
||||
public boolean isDualShock4;
|
||||
public boolean isXboxController;
|
||||
public boolean isXboxBtController;
|
||||
public boolean isNonStandardDualShock4;
|
||||
public boolean usesLinuxGamepadStandardFaceButtons;
|
||||
public boolean isNonStandardXboxBtController;
|
||||
public boolean isServal;
|
||||
public boolean backIsStart;
|
||||
public boolean modeIsSelect;
|
||||
public boolean ignoreBack;
|
||||
public boolean hasJoystickAxes;
|
||||
public boolean pendingExit;
|
||||
|
||||
public int emulatingButtonFlags = 0;
|
||||
|
||||
@@ -1410,7 +1755,25 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
public long lastRbUpTime = 0;
|
||||
|
||||
public long startDownTime = 0;
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
||||
if (vibrator != null) {
|
||||
vibrator.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UsbDeviceContext extends GenericControllerContext {}
|
||||
class UsbDeviceContext extends GenericControllerContext {
|
||||
public AbstractController device;
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
||||
// Nothing for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,263 +8,290 @@ import android.view.KeyEvent;
|
||||
* @author Cameron Gutman
|
||||
*/
|
||||
public class KeyboardTranslator {
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private static final short KEY_PREFIX = (short) 0x80;
|
||||
|
||||
public static final int VK_0 = 48;
|
||||
public static final int VK_9 = 57;
|
||||
public static final int VK_A = 65;
|
||||
public static final int VK_Z = 90;
|
||||
public static final int VK_NUMPAD0 = 96;
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private static final short KEY_PREFIX = (short) 0x80;
|
||||
|
||||
public static final int VK_0 = 48;
|
||||
public static final int VK_9 = 57;
|
||||
public static final int VK_A = 65;
|
||||
public static final int VK_Z = 90;
|
||||
public static final int VK_NUMPAD0 = 96;
|
||||
public static final int VK_BACK_SLASH = 92;
|
||||
public static final int VK_CAPS_LOCK = 20;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
public static final int VK_BACK_SPACE = 8;
|
||||
public static final int VK_EQUALS = 61;
|
||||
public static final int VK_ESCAPE = 27;
|
||||
public static final int VK_F1 = 112;
|
||||
public static final int VK_END = 35;
|
||||
public static final int VK_HOME = 36;
|
||||
public static final int VK_NUM_LOCK = 144;
|
||||
public static final int VK_PAGE_UP = 33;
|
||||
public static final int VK_PAGE_DOWN = 34;
|
||||
public static final int VK_PLUS = 521;
|
||||
public static final int VK_CLOSE_BRACKET = 93;
|
||||
public static final int VK_SCROLL_LOCK = 145;
|
||||
public static final int VK_SEMICOLON = 59;
|
||||
public static final int VK_SLASH = 47;
|
||||
public static final int VK_SPACE = 32;
|
||||
public static final int VK_PRINTSCREEN = 154;
|
||||
public static final int VK_TAB = 9;
|
||||
public static final int VK_LEFT = 37;
|
||||
public static final int VK_RIGHT = 39;
|
||||
public static final int VK_UP = 38;
|
||||
public static final int VK_DOWN = 40;
|
||||
public static final int VK_BACK_QUOTE = 192;
|
||||
public static final int VK_QUOTE = 222;
|
||||
public static final int VK_PAUSE = 19;
|
||||
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||
keycode <= KeyEvent.KEYCODE_Z) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||
keycode <= KeyEvent.KEYCODE_F12) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||
}
|
||||
else {
|
||||
switch (keycode) {
|
||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
public static final int VK_BACK_SPACE = 8;
|
||||
public static final int VK_EQUALS = 61;
|
||||
public static final int VK_ESCAPE = 27;
|
||||
public static final int VK_F1 = 112;
|
||||
public static final int VK_END = 35;
|
||||
public static final int VK_HOME = 36;
|
||||
public static final int VK_NUM_LOCK = 144;
|
||||
public static final int VK_PAGE_UP = 33;
|
||||
public static final int VK_PAGE_DOWN = 34;
|
||||
public static final int VK_PLUS = 521;
|
||||
public static final int VK_CLOSE_BRACKET = 93;
|
||||
public static final int VK_SCROLL_LOCK = 145;
|
||||
public static final int VK_SEMICOLON = 59;
|
||||
public static final int VK_SLASH = 47;
|
||||
public static final int VK_SPACE = 32;
|
||||
public static final int VK_PRINTSCREEN = 154;
|
||||
public static final int VK_TAB = 9;
|
||||
public static final int VK_LEFT = 37;
|
||||
public static final int VK_RIGHT = 39;
|
||||
public static final int VK_UP = 38;
|
||||
public static final int VK_DOWN = 40;
|
||||
public static final int VK_BACK_QUOTE = 192;
|
||||
public static final int VK_QUOTE = 222;
|
||||
public static final int VK_PAUSE = 19;
|
||||
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = 0xA5;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BACKSLASH:
|
||||
translated = 0xdc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||
translated = VK_CAPS_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CLEAR:
|
||||
translated = VK_CLEAR;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_COMMA:
|
||||
translated = 0xbc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
public static boolean needsShift(int keycode) {
|
||||
switch (keycode)
|
||||
{
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
return true;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
translated = VK_BACK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ESCAPE:
|
||||
translated = VK_ESCAPE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
translated = 0x5b;
|
||||
break;
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||
keycode <= KeyEvent.KEYCODE_Z) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||
}
|
||||
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||
keycode <= KeyEvent.KEYCODE_F12) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||
}
|
||||
else {
|
||||
switch (keycode) {
|
||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = 0xA5;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BACKSLASH:
|
||||
translated = 0xdc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||
translated = VK_CAPS_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CLEAR:
|
||||
translated = VK_CLEAR;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_COMMA:
|
||||
translated = 0xbc;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MINUS:
|
||||
translated = 0xbd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_END:
|
||||
translated = VK_END;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||
translated = VK_HOME;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||
translated = VK_NUM_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||
translated = VK_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_UP:
|
||||
translated = VK_PAGE_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PERIOD:
|
||||
translated = 0xbe;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||
translated = 0xdd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||
translated = VK_SCROLL_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SEMICOLON:
|
||||
translated = 0xba;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||
translated = 0xA0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
translated = VK_BACK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = 0xA1;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SLASH:
|
||||
translated = 0xbf;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SPACE:
|
||||
translated = VK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SYSRQ:
|
||||
// Android defines this as SysRq/PrntScrn
|
||||
translated = VK_PRINTSCREEN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_TAB:
|
||||
translated = VK_TAB;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
translated = VK_LEFT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
translated = VK_RIGHT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
translated = VK_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
translated = VK_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_GRAVE:
|
||||
translated = VK_BACK_QUOTE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||
translated = 0xde;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BREAK:
|
||||
translated = VK_PAUSE;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ESCAPE:
|
||||
translated = VK_ESCAPE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
translated = 0x5b;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_MINUS:
|
||||
translated = 0xbd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_END:
|
||||
translated = VK_END;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||
translated = VK_HOME;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||
translated = VK_NUM_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||
translated = VK_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PAGE_UP:
|
||||
translated = VK_PAGE_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PERIOD:
|
||||
translated = 0xbe;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||
translated = 0xdd;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||
translated = VK_SCROLL_LOCK;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SEMICOLON:
|
||||
translated = 0xba;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||
translated = 0xA0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = 0xA1;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SLASH:
|
||||
translated = 0xbf;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SPACE:
|
||||
translated = VK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SYSRQ:
|
||||
// Android defines this as SysRq/PrntScrn
|
||||
translated = VK_PRINTSCREEN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_TAB:
|
||||
translated = VK_TAB;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
translated = VK_LEFT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
translated = VK_RIGHT;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_UP:
|
||||
translated = VK_UP;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||
translated = VK_DOWN;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_GRAVE:
|
||||
translated = VK_BACK_QUOTE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||
translated = 0xde;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BREAK:
|
||||
translated = VK_PAUSE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (short) ((KEY_PREFIX << 8) | translated);
|
||||
}
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
translated = 2 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
translated = 3 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
translated = 8 + VK_0;
|
||||
break;
|
||||
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (short) ((KEY_PREFIX << 8) | translated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
// We extend AndroidPointerIconCaptureProvider because we want to also get the
|
||||
// pointer icon hiding behavior over our stream view just in case pointer capture
|
||||
// is unavailable on this system (ex: DeX, ChromeOS)
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider {
|
||||
private View targetView;
|
||||
|
||||
public AndroidNativePointerCaptureProvider(View targetView) {
|
||||
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
|
||||
super(activity, targetView);
|
||||
this.targetView = targetView;
|
||||
}
|
||||
|
||||
@@ -31,11 +36,6 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
||||
targetView.releasePointerCapture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCapturingActive() {
|
||||
return targetView.hasPointerCapture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
||||
|
||||
@@ -7,55 +7,30 @@ 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 AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||
private ViewGroup rootViewGroup;
|
||||
private View targetView;
|
||||
private Context context;
|
||||
|
||||
public AndroidPointerIconCaptureProvider(Activity activity) {
|
||||
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
|
||||
this.context = activity;
|
||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||
this.targetView = targetView;
|
||||
}
|
||||
|
||||
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() {
|
||||
super.enableCapture();
|
||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.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);
|
||||
targetView.setPointerIcon(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class InputCaptureManager {
|
||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Android O+ native mouse capture");
|
||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||
return new AndroidNativePointerCaptureProvider(activity, activity.findViewById(R.id.surfaceView));
|
||||
}
|
||||
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
||||
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
||||
@@ -28,7 +28,7 @@ public class InputCaptureManager {
|
||||
// Android N's native capture can't capture over system UI elements
|
||||
// so we want to only use it if there's no other option.
|
||||
LimeLog.info("Using Android N+ pointer hiding");
|
||||
return new AndroidPointerIconCaptureProvider(activity);
|
||||
return new AndroidPointerIconCaptureProvider(activity, activity.findViewById(R.id.surfaceView));
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Mouse capture not available");
|
||||
|
||||
@@ -75,8 +75,10 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||
// All mouse events should use relative axes, even if they are zero. This avoids triggering
|
||||
// cursor jumps if we get an event with no associated motion, like ACTION_DOWN or ACTION_UP.
|
||||
return event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
|
||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.limelight.binding.input.driver;
|
||||
public abstract class AbstractController {
|
||||
|
||||
private final int deviceId;
|
||||
private final int vendorId;
|
||||
private final int productId;
|
||||
|
||||
private UsbDriverListener listener;
|
||||
|
||||
@@ -15,6 +17,14 @@ public abstract class AbstractController {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public int getVendorId() {
|
||||
return vendorId;
|
||||
}
|
||||
|
||||
public int getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
protected void setButtonFlag(int buttonFlag, int data) {
|
||||
if (data != 0) {
|
||||
buttonFlags |= buttonFlag;
|
||||
@@ -32,16 +42,20 @@ public abstract class AbstractController {
|
||||
public abstract boolean start();
|
||||
public abstract void stop();
|
||||
|
||||
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||
public AbstractController(int deviceId, UsbDriverListener listener, int vendorId, int productId) {
|
||||
this.deviceId = deviceId;
|
||||
this.listener = listener;
|
||||
this.vendorId = vendorId;
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||
|
||||
protected void notifyDeviceRemoved() {
|
||||
listener.deviceRemoved(deviceId);
|
||||
listener.deviceRemoved(this);
|
||||
}
|
||||
|
||||
protected void notifyDeviceAdded() {
|
||||
listener.deviceAdded(deviceId);
|
||||
listener.deviceAdded(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
protected UsbEndpoint inEndpt, outEndpt;
|
||||
|
||||
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(deviceId, listener);
|
||||
super(deviceId, listener, device.getVendorId(), device.getProductId());
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
}
|
||||
@@ -30,6 +30,18 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
private Thread createInputThread() {
|
||||
return new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
// Delay for a moment before reporting the new gamepad and
|
||||
// accepting new input. This allows time for the old InputDevice
|
||||
// to go away before we reclaim its spot. If the old device is still
|
||||
// around when we call notifyDeviceAdded(), we won't be able to claim
|
||||
// the controller number used by the original InputDevice.
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {}
|
||||
|
||||
// Report that we're added _before_ reporting input
|
||||
notifyDeviceAdded();
|
||||
|
||||
while (!isInterrupted() && !stopped) {
|
||||
byte[] buffer = new byte[64];
|
||||
|
||||
@@ -114,9 +126,6 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Report that we're added _before_ starting the input thread
|
||||
notifyDeviceAdded();
|
||||
|
||||
// Start listening for controller input
|
||||
inputThread = createInputThread();
|
||||
inputThread.start();
|
||||
@@ -131,6 +140,9 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
|
||||
stopped = true;
|
||||
|
||||
// Cancel any rumble effects
|
||||
rumble((short)0, (short)0);
|
||||
|
||||
// Stop the input thread
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
|
||||
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
|
||||
float rightStickX, float rightStickY,
|
||||
float leftTrigger, float rightTrigger);
|
||||
|
||||
void deviceRemoved(int controllerId);
|
||||
void deviceAdded(int controllerId);
|
||||
void deviceRemoved(AbstractController controller);
|
||||
void deviceAdded(AbstractController controller);
|
||||
}
|
||||
|
||||
@@ -47,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceRemoved(int controllerId) {
|
||||
public void deviceRemoved(AbstractController controller) {
|
||||
// Remove the the controller from our list (if not removed already)
|
||||
for (AbstractController controller : controllers) {
|
||||
if (controller.getControllerId() == controllerId) {
|
||||
controllers.remove(controller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
controllers.remove(controller);
|
||||
|
||||
// Call through to the client's listener
|
||||
if (listener != null) {
|
||||
listener.deviceRemoved(controllerId);
|
||||
listener.deviceRemoved(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceAdded(int controllerId) {
|
||||
public void deviceAdded(AbstractController controller) {
|
||||
// Call through to the client's listener
|
||||
if (listener != null) {
|
||||
listener.deviceAdded(controllerId);
|
||||
listener.deviceAdded(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +108,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
// Report all controllerMap that already exist
|
||||
if (listener != null) {
|
||||
for (AbstractController controller : controllers) {
|
||||
listener.deviceAdded(controller.getControllerId());
|
||||
listener.deviceAdded(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +166,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
||||
public static 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.
|
||||
@@ -196,10 +191,32 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean kernelSupportsXboxOne() {
|
||||
String kernelVersion = System.getProperty("os.version");
|
||||
LimeLog.info("Kernel Version: "+kernelVersion);
|
||||
|
||||
if (kernelVersion == null) {
|
||||
// We'll assume this is some newer version of Android
|
||||
// that doesn't let you read the kernel version this way.
|
||||
return true;
|
||||
}
|
||||
else if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.")) {
|
||||
// These are old kernels that definitely don't support Xbox One controllers properly
|
||||
return false;
|
||||
}
|
||||
else if (kernelVersion.startsWith("4.4.") || kernelVersion.startsWith("4.9.")) {
|
||||
// These aren't guaranteed to have backported kernel patches for proper Xbox One
|
||||
// support (though some devices will).
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// The next AOSP common kernel is 4.14 which has working Xbox One controller support
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||
// 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) ||
|
||||
return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && XboxOneController.canClaimDevice(device)) ||
|
||||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||
|
||||
private static final int[] SUPPORTED_VENDORS = {
|
||||
0x0079, // GPD Win 2
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
@@ -23,6 +24,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
0x07ff, // Mad Catz
|
||||
0x0e6f, // Unknown
|
||||
0x0f0d, // Hori
|
||||
0x1038, // SteelSeries
|
||||
0x11c9, // Nacon
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
@@ -137,4 +139,17 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
// No need to fail init if the LED command fails
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||
byte[] data = {
|
||||
0x00, 0x08, 0x00,
|
||||
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
|
||||
0x00, 0x00, 0x00
|
||||
};
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Rumble transfer failed: "+res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
@@ -23,8 +24,34 @@ public class XboxOneController extends AbstractXboxController {
|
||||
0x24c6, // PowerA
|
||||
};
|
||||
|
||||
// FIXME: odata_serial
|
||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
|
||||
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
||||
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
||||
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
||||
private static final byte[] PDP_INIT2 = {0x06, 0x20, 0x00, 0x02, 0x01, 0x00};
|
||||
private static final byte[] RUMBLE_INIT1 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
||||
0x1D, 0x1D, (byte)0xFF, 0x00, 0x00};
|
||||
private static final byte[] RUMBLE_INIT2 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
private static InitPacket[] INIT_PKTS = {
|
||||
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
||||
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
||||
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
||||
new InitPacket(0x045e, 0x02ea, ONE_S_INIT),
|
||||
new InitPacket(0x045e, 0x0b00, ONE_S_INIT),
|
||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT1),
|
||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT1),
|
||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT2),
|
||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT2),
|
||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
|
||||
};
|
||||
|
||||
private byte seqNum = 0;
|
||||
|
||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(device, connection, deviceId, listener);
|
||||
@@ -111,13 +138,55 @@ public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
@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;
|
||||
// Send all applicable init packets
|
||||
for (InitPacket pkt : INIT_PKTS) {
|
||||
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
|
||||
|
||||
// Populate sequence number
|
||||
data[2] = seqNum++;
|
||||
|
||||
// Send the initialization packet
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Initialization transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||
byte[] data = {
|
||||
0x09, 0x00, seqNum++, 0x09, 0x00,
|
||||
0x0F, 0x00, 0x00,
|
||||
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
|
||||
(byte)0xFF, 0x00, (byte)0xFF
|
||||
};
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Rumble transfer failed: "+res);
|
||||
}
|
||||
}
|
||||
|
||||
private static class InitPacket {
|
||||
final int vendorId;
|
||||
final int productId;
|
||||
final byte[] data;
|
||||
|
||||
InitPacket(int vendorId, int productId, byte[] data) {
|
||||
this.vendorId = vendorId;
|
||||
this.productId = productId;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ public interface EvdevListener {
|
||||
int BUTTON_LEFT = 1;
|
||||
int BUTTON_MIDDLE = 2;
|
||||
int BUTTON_RIGHT = 3;
|
||||
int BUTTON_X1 = 4;
|
||||
int BUTTON_X2 = 5;
|
||||
|
||||
void mouseMove(int deltaX, int deltaY);
|
||||
void mouseButtonEvent(int buttonId, boolean down);
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.limelight.binding.input.touch;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class AbsoluteTouchContext implements TouchContext {
|
||||
private int lastTouchDownX = 0;
|
||||
private int lastTouchDownY = 0;
|
||||
private long lastTouchDownTime = 0;
|
||||
private int lastTouchUpX = 0;
|
||||
private int lastTouchUpY = 0;
|
||||
private long lastTouchUpTime = 0;
|
||||
private int lastTouchLocationX = 0;
|
||||
private int lastTouchLocationY = 0;
|
||||
private boolean cancelled;
|
||||
private boolean confirmedLongPress;
|
||||
private boolean confirmedTap;
|
||||
private Timer longPressTimer;
|
||||
private Timer tapDownTimer;
|
||||
private float accumulatedScrollDelta;
|
||||
|
||||
private final NvConnection conn;
|
||||
private final int actionIndex;
|
||||
private final View targetView;
|
||||
|
||||
private static final int SCROLL_SPEED_DIVISOR = 20;
|
||||
|
||||
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
|
||||
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
|
||||
|
||||
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
|
||||
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
|
||||
|
||||
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
|
||||
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
|
||||
|
||||
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
this.targetView = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActionIndex()
|
||||
{
|
||||
return actionIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
|
||||
{
|
||||
if (!isNewFinger) {
|
||||
// We don't handle finger transitions for absolute mode
|
||||
return true;
|
||||
}
|
||||
|
||||
lastTouchLocationX = lastTouchDownX = eventX;
|
||||
lastTouchLocationY = lastTouchDownY = eventY;
|
||||
lastTouchDownTime = SystemClock.uptimeMillis();
|
||||
cancelled = confirmedTap = confirmedLongPress = false;
|
||||
accumulatedScrollDelta = 0;
|
||||
|
||||
if (actionIndex == 0) {
|
||||
// Start the timers
|
||||
startTapDownTimer();
|
||||
startLongPressTimer();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
|
||||
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
|
||||
}
|
||||
|
||||
private void updatePosition(int eventX, int eventY) {
|
||||
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
|
||||
// Normalize these to the view size. We can't just drop them because we won't always get an event
|
||||
// right at the boundary of the view, so dropping them would result in our cursor never really
|
||||
// reaching the sides of the screen.
|
||||
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
|
||||
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
|
||||
|
||||
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUpEvent(int eventX, int eventY)
|
||||
{
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionIndex == 0) {
|
||||
// Cancel the timers
|
||||
cancelLongPressTimer();
|
||||
cancelTapDownTimer();
|
||||
|
||||
// Raise the mouse buttons that we currently have down
|
||||
if (confirmedLongPress) {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
else if (confirmedTap) {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
else {
|
||||
// If we get here, this means that the tap completed within the touch down
|
||||
// deadzone time. We'll need to send the touch down and up events now at the
|
||||
// original touch down position.
|
||||
tapConfirmed();
|
||||
try {
|
||||
// FIXME: Sleeping on the main thread sucks
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException ignored) {}
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
lastTouchLocationX = lastTouchUpX = eventX;
|
||||
lastTouchLocationY = lastTouchUpY = eventY;
|
||||
lastTouchUpTime = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
private synchronized void startLongPressTimer() {
|
||||
longPressTimer = new Timer(true);
|
||||
longPressTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (AbsoluteTouchContext.this) {
|
||||
// Check if someone cancelled us
|
||||
if (longPressTimer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Uncancellable now
|
||||
longPressTimer = null;
|
||||
|
||||
// This timer should have already expired, but cancel it just in case
|
||||
cancelTapDownTimer();
|
||||
|
||||
// Switch from a left click to a right click after a long press
|
||||
confirmedLongPress = true;
|
||||
if (confirmedTap) {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
}
|
||||
}, LONG_PRESS_TIME_THRESHOLD);
|
||||
}
|
||||
|
||||
private synchronized void cancelLongPressTimer() {
|
||||
if (longPressTimer != null) {
|
||||
longPressTimer.cancel();
|
||||
longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startTapDownTimer() {
|
||||
tapDownTimer = new Timer(true);
|
||||
tapDownTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (AbsoluteTouchContext.this) {
|
||||
// Check if someone cancelled us
|
||||
if (tapDownTimer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Uncancellable now
|
||||
tapDownTimer = null;
|
||||
|
||||
// Start our tap
|
||||
tapConfirmed();
|
||||
}
|
||||
}
|
||||
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
|
||||
}
|
||||
|
||||
private synchronized void cancelTapDownTimer() {
|
||||
if (tapDownTimer != null) {
|
||||
tapDownTimer.cancel();
|
||||
tapDownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void tapConfirmed() {
|
||||
if (confirmedTap || confirmedLongPress) {
|
||||
return;
|
||||
}
|
||||
|
||||
confirmedTap = true;
|
||||
cancelTapDownTimer();
|
||||
|
||||
// Left button down at original position
|
||||
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
|
||||
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
|
||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||
updatePosition(lastTouchDownX, lastTouchDownY);
|
||||
}
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean touchMoveEvent(int eventX, int eventY)
|
||||
{
|
||||
if (cancelled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actionIndex == 0) {
|
||||
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
|
||||
// Moved too far since touch down. Cancel the long press timer.
|
||||
cancelLongPressTimer();
|
||||
}
|
||||
|
||||
// Ignore motion within the deadzone period after touch down
|
||||
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
|
||||
tapConfirmed();
|
||||
updatePosition(eventX, eventY);
|
||||
}
|
||||
}
|
||||
else if (actionIndex == 1) {
|
||||
accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR;
|
||||
if ((short)accumulatedScrollDelta != 0) {
|
||||
conn.sendMouseHighResScroll((short)accumulatedScrollDelta);
|
||||
accumulatedScrollDelta -= (short)accumulatedScrollDelta;
|
||||
}
|
||||
}
|
||||
|
||||
lastTouchLocationX = eventX;
|
||||
lastTouchLocationY = eventY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTouch() {
|
||||
cancelled = true;
|
||||
|
||||
// Cancel the timers
|
||||
cancelLongPressTimer();
|
||||
cancelTapDownTimer();
|
||||
|
||||
// Raise the mouse buttons
|
||||
if (confirmedLongPress) {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
else if (confirmedTap) {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPointerCount(int pointerCount) {
|
||||
if (actionIndex == 0 && pointerCount > 1) {
|
||||
cancelTouch();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.limelight.binding.input;
|
||||
package com.limelight.binding.input.touch;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
@@ -8,7 +9,7 @@ import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class TouchContext {
|
||||
public class RelativeTouchContext implements TouchContext {
|
||||
private int lastTouchX = 0;
|
||||
private int lastTouchY = 0;
|
||||
private int originalTouchX = 0;
|
||||
@@ -32,8 +33,8 @@ public class TouchContext {
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||
|
||||
public TouchContext(NvConnection conn, int actionIndex,
|
||||
int referenceWidth, int referenceHeight, View view)
|
||||
public RelativeTouchContext(NvConnection conn, int actionIndex,
|
||||
int referenceWidth, int referenceHeight, View view)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
@@ -42,6 +43,7 @@ public class TouchContext {
|
||||
this.targetView = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActionIndex()
|
||||
{
|
||||
return actionIndex;
|
||||
@@ -57,7 +59,7 @@ public class TouchContext {
|
||||
|
||||
private boolean isTap()
|
||||
{
|
||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
|
||||
|
||||
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
||||
}
|
||||
@@ -72,7 +74,8 @@ public class TouchContext {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean touchDownEvent(int eventX, int eventY)
|
||||
@Override
|
||||
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
|
||||
{
|
||||
// Get the view dimensions to scale inputs on this touch
|
||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||
@@ -80,7 +83,7 @@ public class TouchContext {
|
||||
|
||||
originalTouchX = lastTouchX = eventX;
|
||||
originalTouchY = lastTouchY = eventY;
|
||||
originalTouchTime = System.currentTimeMillis();
|
||||
originalTouchTime = SystemClock.uptimeMillis();
|
||||
cancelled = confirmedDrag = confirmedMove = false;
|
||||
distanceMoved = 0;
|
||||
|
||||
@@ -92,6 +95,7 @@ public class TouchContext {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touchUpEvent(int eventX, int eventY)
|
||||
{
|
||||
if (cancelled) {
|
||||
@@ -128,7 +132,7 @@ public class TouchContext {
|
||||
dragTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (TouchContext.this) {
|
||||
synchronized (RelativeTouchContext.this) {
|
||||
// Check if someone already set move
|
||||
if (confirmedMove) {
|
||||
return;
|
||||
@@ -179,6 +183,7 @@ public class TouchContext {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean touchMoveEvent(int eventX, int eventY)
|
||||
{
|
||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||
@@ -223,6 +228,7 @@ public class TouchContext {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTouch() {
|
||||
cancelled = true;
|
||||
|
||||
@@ -235,7 +241,11 @@ public class TouchContext {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPointerCount(int pointerCount) {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.limelight.binding.input.touch;
|
||||
|
||||
public interface TouchContext {
|
||||
int getActionIndex();
|
||||
void setPointerCount(int pointerCount);
|
||||
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
|
||||
boolean touchMoveEvent(int eventX, int eventY);
|
||||
void touchUpEvent(int eventX, int eventY);
|
||||
void cancelTouch();
|
||||
boolean isCancelled();
|
||||
}
|
||||
@@ -293,12 +293,12 @@ public class AnalogStick extends VirtualControllerElement {
|
||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||
movement_angle = getAngle(relative_x, relative_y);
|
||||
|
||||
// chop radius if out of outer circle and already pressed
|
||||
// pass touch event to parent if out of outer circle
|
||||
if (movement_radius > radius_complete && !isPressed())
|
||||
return false;
|
||||
|
||||
// chop radius if out of outer circle or near the edge
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -338,6 +338,7 @@ public class AnalogStick extends VirtualControllerElement {
|
||||
} else {
|
||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
notifyOnRevoke();
|
||||
|
||||
// not longer pressed reset analog stick
|
||||
notifyOnMovement(0, 0);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
@@ -60,6 +61,7 @@ public class DigitalButton extends VirtualControllerElement {
|
||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
private final RectF rect = new RectF();
|
||||
|
||||
private int layer;
|
||||
private DigitalButton movingButton = null;
|
||||
@@ -144,14 +146,18 @@ public class DigitalButton extends VirtualControllerElement {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setTextSize(getPercent(getWidth(), 30));
|
||||
paint.setTextSize(getPercent(getWidth(), 25));
|
||||
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);
|
||||
|
||||
rect.left = rect.top = paint.getStrokeWidth();
|
||||
rect.right = getWidth() - rect.left;
|
||||
rect.bottom = getHeight() - rect.top;
|
||||
|
||||
canvas.drawOval(rect, paint);
|
||||
|
||||
if (icon != -1) {
|
||||
Drawable d = getResources().getDrawable(icon);
|
||||
|
||||
@@ -13,10 +13,13 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.input.ControllerHandler;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class VirtualController {
|
||||
public class ControllerInputContext {
|
||||
@@ -31,17 +34,20 @@ public class VirtualController {
|
||||
|
||||
public enum ControllerMode {
|
||||
Active,
|
||||
Configuration
|
||||
MoveButtons,
|
||||
ResizeButtons
|
||||
}
|
||||
|
||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
|
||||
private NvConnection connection = null;
|
||||
private ControllerHandler controllerHandler;
|
||||
private Context context = null;
|
||||
|
||||
private FrameLayout frame_layout = null;
|
||||
private RelativeLayout relative_layout = null;
|
||||
|
||||
private Timer retransmitTimer;
|
||||
|
||||
ControllerMode currentMode = ControllerMode.Active;
|
||||
ControllerInputContext inputContext = new ControllerInputContext();
|
||||
|
||||
@@ -49,8 +55,8 @@ public class VirtualController {
|
||||
|
||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||
|
||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||
this.connection = conn;
|
||||
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
||||
this.controllerHandler = controllerHandler;
|
||||
this.frame_layout = layout;
|
||||
this.context = context;
|
||||
|
||||
@@ -60,19 +66,23 @@ public class VirtualController {
|
||||
|
||||
buttonConfigure = new Button(context);
|
||||
buttonConfigure.setAlpha(0.25f);
|
||||
buttonConfigure.setFocusable(false);
|
||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String message;
|
||||
|
||||
if (currentMode == ControllerMode.Configuration) {
|
||||
if (currentMode == ControllerMode.Active){
|
||||
currentMode = ControllerMode.MoveButtons;
|
||||
message = "Entering configuration mode (Move buttons)";
|
||||
} else if (currentMode == ControllerMode.MoveButtons) {
|
||||
currentMode = ControllerMode.ResizeButtons;
|
||||
message = "Entering configuration mode (Resize buttons)";
|
||||
} else {
|
||||
currentMode = ControllerMode.Active;
|
||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||
message = "Exiting configuration mode";
|
||||
} else {
|
||||
currentMode = ControllerMode.Configuration;
|
||||
message = "Entering configuration mode";
|
||||
}
|
||||
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
@@ -84,14 +94,28 @@ public class VirtualController {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
retransmitTimer.cancel();
|
||||
relative_layout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
relative_layout.setVisibility(View.VISIBLE);
|
||||
|
||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
||||
// very shortly after another. This can be critical if an axis zeroing packet
|
||||
// is lost and causes an analog stick to get stuck. To avoid this, we send
|
||||
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
|
||||
retransmitTimer = new Timer("OSC timer", true);
|
||||
retransmitTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendControllerInputContext();
|
||||
}
|
||||
}, 100, 100);
|
||||
}
|
||||
|
||||
public void removeElements() {
|
||||
@@ -101,6 +125,13 @@ public class VirtualController {
|
||||
elements.clear();
|
||||
}
|
||||
|
||||
public void setOpacity(int opacity) {
|
||||
for (VirtualControllerElement element : elements) {
|
||||
element.setOpacity(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||
elements.add(element);
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||
@@ -148,32 +179,23 @@ public class VirtualController {
|
||||
return inputContext;
|
||||
}
|
||||
|
||||
public void sendControllerInputContext() {
|
||||
sendControllerInputPacket();
|
||||
}
|
||||
void sendControllerInputContext() {
|
||||
_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);
|
||||
|
||||
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();
|
||||
if (controllerHandler != null) {
|
||||
controllerHandler.reportOscState(
|
||||
inputContext.inputMap,
|
||||
inputContext.leftStickX,
|
||||
inputContext.leftStickY,
|
||||
inputContext.rightStickX,
|
||||
inputContext.rightStickY,
|
||||
inputContext.leftTrigger,
|
||||
inputContext.rightTrigger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ public class VirtualControllerConfigurationLoader {
|
||||
return (int) (((float) total / (float) 100) * (float) percent);
|
||||
}
|
||||
|
||||
// The default controls are specified using a grid of 128*72 cells at 16:9
|
||||
private static int screenScale(int units, int height) {
|
||||
return (int) (((float) height / (float) 72) * (float) units);
|
||||
}
|
||||
|
||||
private static DigitalPad createDigitalPad(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
@@ -145,149 +150,186 @@ public class VirtualControllerConfigurationLoader {
|
||||
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);
|
||||
|
||||
private static final int TRIGGER_L_BASE_X = 1;
|
||||
private static final int TRIGGER_R_BASE_X = 92;
|
||||
private static final int TRIGGER_DISTANCE = 23;
|
||||
private static final int TRIGGER_BASE_Y = 31;
|
||||
private static final int TRIGGER_WIDTH = 12;
|
||||
private static final int TRIGGER_HEIGHT = 9;
|
||||
|
||||
// Face buttons are defined based on the Y button (button number 9)
|
||||
private static final int BUTTON_BASE_X = 106;
|
||||
private static final int BUTTON_BASE_Y = 1;
|
||||
private static final int BUTTON_SIZE = 10;
|
||||
|
||||
private static final int DPAD_BASE_X = 4;
|
||||
private static final int DPAD_BASE_Y = 41;
|
||||
private static final int DPAD_SIZE = 30;
|
||||
|
||||
private static final int ANALOG_L_BASE_X = 4;
|
||||
private static final int ANALOG_L_BASE_Y = 1;
|
||||
private static final int ANALOG_R_BASE_X = 96;
|
||||
private static final int ANALOG_R_BASE_Y = 42;
|
||||
private static final int ANALOG_SIZE = 28;
|
||||
|
||||
private static final int L3_R3_BASE_Y = 60;
|
||||
|
||||
private static final int START_X = 83;
|
||||
private static final int BACK_X = 34;
|
||||
private static final int START_BACK_Y = 64;
|
||||
private static final int START_BACK_WIDTH = 12;
|
||||
private static final int START_BACK_HEIGHT = 7;
|
||||
|
||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||
|
||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||
|
||||
// Displace controls on the right by this amount of pixels to account for different aspect ratios
|
||||
int rightDisplacement = screen.widthPixels - screen.heightPixels * 16 / 9;
|
||||
|
||||
int height = screen.heightPixels;
|
||||
|
||||
// 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.
|
||||
|
||||
if (!config.onlyL3R3)
|
||||
{
|
||||
controller.addElement(createDigitalPad(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(30, screen.widthPixels),
|
||||
getPercent(40, screen.heightPixels)
|
||||
screenScale(DPAD_BASE_X, height),
|
||||
screenScale(DPAD_BASE_Y, height),
|
||||
screenScale(DPAD_SIZE, height),
|
||||
screenScale(DPAD_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_A,
|
||||
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)
|
||||
!config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
|
||||
!config.flipFaceButtons ? "A" : "B", -1, controller, context),
|
||||
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_B,
|
||||
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)
|
||||
config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
|
||||
config.flipFaceButtons ? "A" : "B", -1, controller, context),
|
||||
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
|
||||
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_X,
|
||||
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)
|
||||
!config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
|
||||
!config.flipFaceButtons ? "X" : "Y", -1, controller, context),
|
||||
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
|
||||
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_Y,
|
||||
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)
|
||||
config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
|
||||
config.flipFaceButtons ? "X" : "Y", -1, controller, context),
|
||||
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||
screenScale(BUTTON_BASE_Y, height),
|
||||
screenScale(BUTTON_SIZE, height),
|
||||
screenScale(BUTTON_SIZE, height)
|
||||
);
|
||||
|
||||
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)
|
||||
1, "LT", -1, controller, context),
|
||||
screenScale(TRIGGER_L_BASE_X, height),
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
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)
|
||||
1, "RT", -1, controller, context),
|
||||
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_LB,
|
||||
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)
|
||||
screenScale(TRIGGER_L_BASE_X + TRIGGER_DISTANCE, height),
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_RB,
|
||||
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)
|
||||
screenScale(TRIGGER_R_BASE_X, height) + rightDisplacement,
|
||||
screenScale(TRIGGER_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftStick(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
screenScale(ANALOG_L_BASE_X, height),
|
||||
screenScale(ANALOG_L_BASE_Y, height),
|
||||
screenScale(ANALOG_SIZE, height),
|
||||
screenScale(ANALOG_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createRightStick(controller, context),
|
||||
getPercent(55, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
screenScale(ANALOG_R_BASE_X, height) + rightDisplacement,
|
||||
screenScale(ANALOG_R_BASE_Y, height),
|
||||
screenScale(ANALOG_SIZE, height),
|
||||
screenScale(ANALOG_SIZE, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_BACK,
|
||||
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)
|
||||
screenScale(BACK_X, height),
|
||||
screenScale(START_BACK_Y, height),
|
||||
screenScale(START_BACK_WIDTH, height),
|
||||
screenScale(START_BACK_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_START,
|
||||
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)
|
||||
screenScale(START_X, height) + rightDisplacement,
|
||||
screenScale(START_BACK_Y, height),
|
||||
screenScale(START_BACK_WIDTH, height),
|
||||
screenScale(START_BACK_HEIGHT, height)
|
||||
);
|
||||
}
|
||||
else {
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_LSB,
|
||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
||||
getPercent(2, screen.widthPixels),
|
||||
getPercent(80, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
screenScale(TRIGGER_L_BASE_X, height),
|
||||
screenScale(L3_R3_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_RSB,
|
||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
||||
getPercent(89, screen.widthPixels),
|
||||
getPercent(80, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||
screenScale(L3_R3_BASE_Y, height),
|
||||
screenScale(TRIGGER_WIDTH, height),
|
||||
screenScale(TRIGGER_HEIGHT, height)
|
||||
);
|
||||
}
|
||||
|
||||
controller.setOpacity(config.oscOpacity);
|
||||
}
|
||||
|
||||
public static void saveProfile(final VirtualController controller,
|
||||
@@ -306,7 +348,7 @@ public class VirtualControllerConfigurationLoader {
|
||||
prefEditor.apply();
|
||||
}
|
||||
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
||||
|
||||
for (VirtualControllerElement element : controller.getElements()) {
|
||||
@@ -324,5 +366,5 @@ public class VirtualControllerConfigurationLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ public abstract class VirtualControllerElement extends View {
|
||||
|
||||
private int normalColor = 0xF0888888;
|
||||
protected int pressedColor = 0xF00000FF;
|
||||
private int configNormalColor = 0xF0FF0000;
|
||||
private int configMoveColor = 0xF0FF0000;
|
||||
private int configResizeColor = 0xF0FF00FF;
|
||||
private int configSelectedColor = 0xF000FF00;
|
||||
|
||||
protected int startSize_x;
|
||||
@@ -112,34 +113,34 @@ public abstract class VirtualControllerElement extends View {
|
||||
|
||||
/*
|
||||
protected void actionShowNormalColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
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();
|
||||
}
|
||||
@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) {
|
||||
}
|
||||
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();
|
||||
}
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
*/
|
||||
|
||||
protected void actionEnableMove() {
|
||||
@@ -156,8 +157,12 @@ public abstract class VirtualControllerElement extends View {
|
||||
}
|
||||
|
||||
protected int getDefaultColor() {
|
||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||
configNormalColor : normalColor;
|
||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||
return configMoveColor;
|
||||
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||
return configResizeColor;
|
||||
else
|
||||
return normalColor;
|
||||
}
|
||||
|
||||
protected int getDefaultStrokeWidth() {
|
||||
@@ -166,58 +171,54 @@ public abstract class VirtualControllerElement extends View {
|
||||
}
|
||||
|
||||
protected void showConfigurationDialog() {
|
||||
try {
|
||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||
|
||||
alertBuilder.setTitle("Configuration");
|
||||
alertBuilder.setTitle("Configuration");
|
||||
|
||||
CharSequence functions[] = new CharSequence[]{
|
||||
"Move",
|
||||
"Resize",
|
||||
/*election
|
||||
"Set n
|
||||
Disable color sormal color",
|
||||
"Set pressed color",
|
||||
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;
|
||||
}
|
||||
*/
|
||||
"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();
|
||||
default: { // cancel
|
||||
actionCancel();
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
AlertDialog alert = alertBuilder.create();
|
||||
// show menu
|
||||
alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -234,7 +235,10 @@ public abstract class VirtualControllerElement extends View {
|
||||
startSize_x = getWidth();
|
||||
startSize_y = getHeight();
|
||||
|
||||
actionEnableMove();
|
||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||
actionEnableMove();
|
||||
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||
actionEnableResize();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -291,6 +295,15 @@ public abstract class VirtualControllerElement extends View {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
||||
public void setOpacity(int opacity) {
|
||||
int hexOpacity = opacity * 255 / 100;
|
||||
this.normalColor = (hexOpacity << 24) | (normalColor & 0x00FFFFFF);
|
||||
this.pressedColor = (hexOpacity << 24) | (pressedColor & 0x00FFFFFF);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected final float getPercent(float value, float percent) {
|
||||
return value / 100 * percent;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
@@ -38,10 +40,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private boolean submittedCsd;
|
||||
private boolean submitCsdNextCall;
|
||||
|
||||
private Context context;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||
private boolean adaptivePlayback, directSubmit;
|
||||
private boolean lowLatency;
|
||||
private boolean constrainedHighProfile;
|
||||
private boolean refFrameInvalidationAvc, refFrameInvalidationHevc;
|
||||
private boolean refFrameInvalidationActive;
|
||||
@@ -55,6 +59,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private String glRenderer;
|
||||
private boolean foreground = true;
|
||||
private boolean legacyFrameDropRendering = false;
|
||||
private PerfOverlayListener perfListener;
|
||||
|
||||
private MediaFormat inputFormat;
|
||||
private MediaFormat outputFormat;
|
||||
private MediaFormat configuredFormat;
|
||||
|
||||
private boolean needsBaselineSpsHack;
|
||||
private SeqParameterSet savedSps;
|
||||
@@ -63,13 +72,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
private long initialExceptionTimestamp;
|
||||
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
||||
|
||||
private VideoStats activeWindowVideoStats;
|
||||
private VideoStats lastWindowVideoStats;
|
||||
private VideoStats globalVideoStats;
|
||||
|
||||
private long lastTimestampUs;
|
||||
private long decoderTimeMs;
|
||||
private long totalTimeMs;
|
||||
private int totalFramesReceived;
|
||||
private int totalFramesRendered;
|
||||
private int frameLossEvents;
|
||||
private int framesLost;
|
||||
private int lastFrameNumber;
|
||||
private int refreshRate;
|
||||
private PreferenceConfiguration prefs;
|
||||
@@ -119,16 +126,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
this.renderTarget = renderTarget;
|
||||
}
|
||||
|
||||
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
|
||||
public MediaCodecDecoderRenderer(Context context, PreferenceConfiguration prefs,
|
||||
CrashListener crashListener, int consecutiveCrashCount,
|
||||
boolean meteredData, boolean requestedHdr,
|
||||
String glRenderer) {
|
||||
String glRenderer, PerfOverlayListener perfListener) {
|
||||
//dumpDecoders();
|
||||
|
||||
this.context = context;
|
||||
this.prefs = prefs;
|
||||
this.crashListener = crashListener;
|
||||
this.consecutiveCrashCount = consecutiveCrashCount;
|
||||
this.glRenderer = glRenderer;
|
||||
this.perfListener = perfListener;
|
||||
|
||||
this.activeWindowVideoStats = new VideoStats();
|
||||
this.lastWindowVideoStats = new VideoStats();
|
||||
this.globalVideoStats = new VideoStats();
|
||||
|
||||
avcDecoder = findAvcDecoder();
|
||||
if (avcDecoder != null) {
|
||||
@@ -153,7 +166,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
// shared between AVC and HEVC decoders on the same device.
|
||||
if (avcDecoder != null) {
|
||||
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
|
||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder);
|
||||
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
|
||||
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
|
||||
|
||||
@@ -182,8 +194,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return avcDecoder != null;
|
||||
}
|
||||
|
||||
public boolean is49FpsBlacklisted() {
|
||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName());
|
||||
public boolean isBlacklistedForFrameRate(int frameRate) {
|
||||
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedForFrameRate(avcDecoder.getName(), frameRate);
|
||||
}
|
||||
|
||||
public void enableLegacyFrameDropRendering() {
|
||||
@@ -256,6 +268,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
refFrameInvalidationActive = refFrameInvalidationAvc;
|
||||
|
||||
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(avcDecoder, mimeType);
|
||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder, mimeType);
|
||||
}
|
||||
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
||||
mimeType = "video/hevc";
|
||||
@@ -267,6 +282,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
refFrameInvalidationActive = refFrameInvalidationHevc;
|
||||
|
||||
lowLatency = MediaCodecHelper.decoderSupportsLowLatency(hevcDecoder, mimeType);
|
||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(hevcDecoder, mimeType);
|
||||
}
|
||||
else {
|
||||
// Unknown format
|
||||
@@ -285,6 +303,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
|
||||
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
|
||||
|
||||
// Avoid setting KEY_FRAME_RATE on Lollipop and earlier to reduce compatibility risk
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// We use prefs.fps instead of redrawRate here because the low latency hack in Game.java
|
||||
// may leave us with an odd redrawRate value like 59 or 49 which might cause the decoder
|
||||
// to puke. To be safe, we'll use the unmodified value.
|
||||
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, prefs.fps);
|
||||
}
|
||||
|
||||
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
||||
// so we don't fill these pre-KitKat
|
||||
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
@@ -292,7 +318,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && lowLatency) {
|
||||
videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Set the Qualcomm vendor low latency extension if the Android R option is unavailable
|
||||
if (MediaCodecHelper.decoderSupportsQcomVendorLowLatency(selectedDecoderName)) {
|
||||
// MediaCodec supports vendor-defined format keys using the "vendor.<extension name>.<parameter name>" syntax.
|
||||
// These allow access to functionality that is not exposed through documented MediaFormat.KEY_* values.
|
||||
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/common/inc/vidc_vendor_extensions.h;l=67
|
||||
//
|
||||
// Examples of Qualcomm's vendor extensions for Snapdragon 845:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/sdm845/media/mm-video-v4l2/vidc/vdec/src/omx_vdec_extensions.hpp
|
||||
// https://cs.android.com/android/_/android/platform/hardware/qcom/sm8150/media/+/0621ceb1c1b19564999db8293574a0e12952ff6c
|
||||
videoFormat.setInteger("vendor.qti-ext-dec-low-latency.enable", 1);
|
||||
}
|
||||
|
||||
// Operate at maximum rate to lower latency as much as possible on
|
||||
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
||||
// but that will actually result in the decoder crashing if it can't satisfy
|
||||
@@ -300,8 +341,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
configuredFormat = videoFormat;
|
||||
LimeLog.info("Configuring with format: "+configuredFormat);
|
||||
|
||||
try {
|
||||
videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// This will contain the actual accepted input format attributes
|
||||
inputFormat = videoDecoder.getInputFormat();
|
||||
LimeLog.info("Input format: "+inputFormat);
|
||||
}
|
||||
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
@@ -311,7 +362,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
if (USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
activeWindowVideoStats.totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +377,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
legacyInputBuffers = videoDecoder.getInputBuffers();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return -5;
|
||||
@@ -405,23 +457,30 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// Render the last buffer
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
|
||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||
// is disabled
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (legacyFrameDropRendering) {
|
||||
// Use a PTS that will cause this frame to be dropped if another comes in within
|
||||
// the same V-sync period
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
|
||||
}
|
||||
else {
|
||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
||||
// is disabled
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
}
|
||||
|
||||
totalFramesRendered++;
|
||||
activeWindowVideoStats.totalFramesRendered++;
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
decoderTimeMs += delta;
|
||||
activeWindowVideoStats.decoderTimeMs += delta;
|
||||
if (!USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
activeWindowVideoStats.totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -430,7 +489,8 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||
outputFormat = videoDecoder.getOutputFormat();
|
||||
LimeLog.info("New output format: " + outputFormat);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -578,17 +638,58 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
return MoonBridge.DR_OK;
|
||||
}
|
||||
|
||||
totalFramesReceived++;
|
||||
|
||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||
// In that case, each frame start NALU is submitted independently.
|
||||
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||
framesLost += frameNumber - lastFrameNumber - 1;
|
||||
frameLossEvents++;
|
||||
if (lastFrameNumber == 0) {
|
||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
||||
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||
// In that case, each frame start NALU is submitted independently.
|
||||
activeWindowVideoStats.framesLost += frameNumber - lastFrameNumber - 1;
|
||||
activeWindowVideoStats.totalFrames += frameNumber - lastFrameNumber - 1;
|
||||
activeWindowVideoStats.frameLossEvents++;
|
||||
}
|
||||
|
||||
lastFrameNumber = frameNumber;
|
||||
|
||||
// Flip stats windows roughly every second
|
||||
if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
|
||||
if (prefs.enablePerfOverlay) {
|
||||
VideoStats lastTwo = new VideoStats();
|
||||
lastTwo.add(lastWindowVideoStats);
|
||||
lastTwo.add(activeWindowVideoStats);
|
||||
VideoStatsFps fps = lastTwo.getFps();
|
||||
String decoder;
|
||||
|
||||
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
|
||||
decoder = avcDecoder.getName();
|
||||
} else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
|
||||
decoder = hevcDecoder.getName();
|
||||
} else {
|
||||
decoder = "(unknown)";
|
||||
}
|
||||
|
||||
float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived;
|
||||
String perfText = context.getString(
|
||||
R.string.perf_overlay_text,
|
||||
initialWidth + "x" + initialHeight,
|
||||
decoder,
|
||||
fps.totalFps,
|
||||
fps.receivedFps,
|
||||
fps.renderedFps,
|
||||
(float)lastTwo.framesLost / lastTwo.totalFrames * 100,
|
||||
((float)lastTwo.totalTimeMs / lastTwo.totalFramesReceived) - decodeTimeMs,
|
||||
decodeTimeMs);
|
||||
perfListener.onPerfUpdate(perfText);
|
||||
}
|
||||
|
||||
globalVideoStats.add(activeWindowVideoStats);
|
||||
lastWindowVideoStats.copy(activeWindowVideoStats);
|
||||
activeWindowVideoStats.clear();
|
||||
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
activeWindowVideoStats.totalFramesReceived++;
|
||||
activeWindowVideoStats.totalFrames++;
|
||||
|
||||
int inputBufferIndex;
|
||||
ByteBuffer buf;
|
||||
|
||||
@@ -596,7 +697,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
|
||||
if (!FRAME_RENDER_TIME_ONLY) {
|
||||
// Count time from first packet received to decode start
|
||||
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
||||
activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
||||
}
|
||||
|
||||
if (timestampUs <= lastTimestampUs) {
|
||||
@@ -627,17 +728,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
// for known resolution combinations. Reference frame invalidation may need
|
||||
// these, so leave them be for those decoders.
|
||||
if (!refFrameInvalidationActive) {
|
||||
if (initialWidth <= 720 && initialHeight <= 480) {
|
||||
if (initialWidth <= 720 && initialHeight <= 480 && refreshRate <= 60) {
|
||||
// Max 5 buffered frames at 720x480x60
|
||||
LimeLog.info("Patching level_idc to 31");
|
||||
sps.levelIdc = 31;
|
||||
}
|
||||
else if (initialWidth <= 1280 && initialHeight <= 720) {
|
||||
else if (initialWidth <= 1280 && initialHeight <= 720 && refreshRate <= 60) {
|
||||
// Max 5 buffered frames at 1280x720x60
|
||||
LimeLog.info("Patching level_idc to 32");
|
||||
sps.levelIdc = 32;
|
||||
}
|
||||
else if (initialWidth <= 1920 && initialHeight <= 1080) {
|
||||
else if (initialWidth <= 1920 && initialHeight <= 1080 && refreshRate <= 60) {
|
||||
// Max 4 buffered frames at 1920x1080x64
|
||||
LimeLog.info("Patching level_idc to 42");
|
||||
sps.levelIdc = 42;
|
||||
@@ -662,12 +763,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
// GFE 2.5.11 changed the SPS to add additional extensions
|
||||
// Some devices don't like these so we remove them here.
|
||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||
// Some devices don't like these so we remove them here on old devices.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
sps.vuiParams.videoSignalTypePresentFlag = false;
|
||||
sps.vuiParams.colourDescriptionPresentFlag = false;
|
||||
sps.vuiParams.chromaLocInfoPresentFlag = false;
|
||||
}
|
||||
|
||||
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
|
||||
// Some older devices used to choke on a bitstream restrictions, so we won't provide them
|
||||
// unless explicitly whitelisted. For newer devices, leave the bitstream restrictions present.
|
||||
if (needsSpsBitstreamFixup || isExynos4 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// 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.
|
||||
|
||||
@@ -903,17 +1008,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
}
|
||||
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (totalFramesReceived == 0) {
|
||||
if (globalVideoStats.totalFramesReceived == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(totalTimeMs / totalFramesReceived);
|
||||
return (int)(globalVideoStats.totalTimeMs / globalVideoStats.totalFramesReceived);
|
||||
}
|
||||
|
||||
public int getAverageDecoderLatency() {
|
||||
if (totalFramesReceived == 0) {
|
||||
if (globalVideoStats.totalFramesReceived == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(decoderTimeMs / totalFramesReceived);
|
||||
return (int)(globalVideoStats.decoderTimeMs / globalVideoStats.totalFramesReceived);
|
||||
}
|
||||
|
||||
static class DecoderHungException extends RuntimeException {
|
||||
@@ -958,25 +1063,46 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
||||
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
|
||||
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
||||
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n";
|
||||
str += "AVC supported width range: "+avcWidthRange+"\n";
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
Range<Double> avcFpsRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
|
||||
str += "AVC achievable FPS range: "+avcFpsRange+"\n";
|
||||
} catch (IllegalArgumentException e) {
|
||||
str += "AVC achievable FPS range: UNSUPPORTED!\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
|
||||
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
||||
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n";
|
||||
str += "HEVC supported width range: "+hevcWidthRange+"\n";
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
Range<Double> hevcFpsRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getAchievableFrameRatesFor(renderer.initialWidth, renderer.initialHeight);
|
||||
str += "HEVC achievable FPS range: " + hevcFpsRange + "\n";
|
||||
} catch (IllegalArgumentException e) {
|
||||
str += "HEVC achievable FPS range: UNSUPPORTED!\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
str += "Configured format: "+renderer.configuredFormat+"\n";
|
||||
str += "Input format: "+renderer.inputFormat+"\n";
|
||||
str += "Output format: "+renderer.outputFormat+"\n";
|
||||
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
|
||||
str += "GL Renderer: "+renderer.glRenderer+"\n";
|
||||
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
|
||||
str += "Foreground: "+renderer.foreground+"\n";
|
||||
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
|
||||
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
|
||||
str += "Using modern SPS patching: "+(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)+"\n";
|
||||
str += "Low latency mode: "+renderer.lowLatency+"\n";
|
||||
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
|
||||
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
|
||||
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
|
||||
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
|
||||
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
|
||||
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
|
||||
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
||||
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
public interface PerfOverlayListener {
|
||||
void onPerfUpdate(final String text);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
class VideoStats {
|
||||
|
||||
long decoderTimeMs;
|
||||
long totalTimeMs;
|
||||
int totalFrames;
|
||||
int totalFramesReceived;
|
||||
int totalFramesRendered;
|
||||
int frameLossEvents;
|
||||
int framesLost;
|
||||
long measurementStartTimestamp;
|
||||
|
||||
void add(VideoStats other) {
|
||||
this.decoderTimeMs += other.decoderTimeMs;
|
||||
this.totalTimeMs += other.totalTimeMs;
|
||||
this.totalFrames += other.totalFrames;
|
||||
this.totalFramesReceived += other.totalFramesReceived;
|
||||
this.totalFramesRendered += other.totalFramesRendered;
|
||||
this.frameLossEvents += other.frameLossEvents;
|
||||
this.framesLost += other.framesLost;
|
||||
|
||||
if (this.measurementStartTimestamp == 0) {
|
||||
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
void copy(VideoStats other) {
|
||||
this.decoderTimeMs = other.decoderTimeMs;
|
||||
this.totalTimeMs = other.totalTimeMs;
|
||||
this.totalFrames = other.totalFrames;
|
||||
this.totalFramesReceived = other.totalFramesReceived;
|
||||
this.totalFramesRendered = other.totalFramesRendered;
|
||||
this.frameLossEvents = other.frameLossEvents;
|
||||
this.framesLost = other.framesLost;
|
||||
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.decoderTimeMs = 0;
|
||||
this.totalTimeMs = 0;
|
||||
this.totalFrames = 0;
|
||||
this.totalFramesReceived = 0;
|
||||
this.totalFramesRendered = 0;
|
||||
this.frameLossEvents = 0;
|
||||
this.framesLost = 0;
|
||||
this.measurementStartTimestamp = 0;
|
||||
}
|
||||
|
||||
VideoStatsFps getFps() {
|
||||
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
||||
|
||||
VideoStatsFps fps = new VideoStatsFps();
|
||||
if (elapsed > 0) {
|
||||
fps.totalFps = this.totalFrames / elapsed;
|
||||
fps.receivedFps = this.totalFramesReceived / elapsed;
|
||||
fps.renderedFps = this.totalFramesRendered / elapsed;
|
||||
}
|
||||
return fps;
|
||||
}
|
||||
}
|
||||
|
||||
class VideoStatsFps {
|
||||
|
||||
float totalFps;
|
||||
float receivedFps;
|
||||
float renderedFps;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import android.content.ContentValues;
|
||||
@@ -17,15 +18,15 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
public class ComputerDatabaseManager {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_DB_NAME = "computers3.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||
private static final String MAC_COLUMN_NAME = "Mac";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
|
||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||
|
||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||
private static final char ADDRESS_DELIMITER = ';';
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
@@ -38,86 +39,98 @@ public class ComputerDatabaseManager {
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
}
|
||||
initializeDb();
|
||||
initializeDb(c);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
computerDb.close();
|
||||
}
|
||||
|
||||
private void initializeDb() {
|
||||
private void initializeDb(Context c) {
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT)",
|
||||
COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
|
||||
// Move all computers from the old DB (if any) to the new one
|
||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||
for (ComputerDetails computer : oldComputers) {
|
||||
updateComputer(computer);
|
||||
}
|
||||
oldComputers = LegacyDatabaseReader2.migrateAllComputers(c);
|
||||
for (ComputerDetails computer : oldComputers) {
|
||||
updateComputer(computer);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||
public void deleteComputer(ComputerDetails details) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
|
||||
}
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
|
||||
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||
|
||||
StringBuilder addresses = new StringBuilder();
|
||||
addresses.append(details.localAddress != null ? details.localAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : "");
|
||||
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : "");
|
||||
|
||||
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
|
||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||
try {
|
||||
if (details.serverCert != null) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
|
||||
}
|
||||
else {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
private static String readNonEmptyString(String input) {
|
||||
if (input.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
details.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
|
||||
|
||||
// An earlier schema defined addresses as byte blobs. We'll
|
||||
// gracefully migrate those to strings so we can store DNS names
|
||||
// too. To disambiguate, we'll need to prefix them with a string
|
||||
// greater than the allowable IP address length.
|
||||
try {
|
||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy local address for "+details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(2);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||
}
|
||||
else {
|
||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
||||
}
|
||||
}
|
||||
details.localAddress = readNonEmptyString(addresses[0]);
|
||||
details.remoteAddress = readNonEmptyString(addresses[1]);
|
||||
details.manualAddress = readNonEmptyString(addresses[2]);
|
||||
details.ipv6Address = readNonEmptyString(addresses[3]);
|
||||
|
||||
details.macAddress = c.getString(3);
|
||||
|
||||
try {
|
||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy remote address for "+details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(3);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||
}
|
||||
else {
|
||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
||||
}
|
||||
}
|
||||
byte[] derCertData = c.getBlob(4);
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
if (derCertData != null) {
|
||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
@@ -126,16 +139,7 @@ public class ComputerDatabaseManager {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||
details.macAddress == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
computerList.add(details);
|
||||
computerList.add(getComputerFromCursor(c));
|
||||
}
|
||||
|
||||
c.close();
|
||||
@@ -143,8 +147,8 @@ public class ComputerDatabaseManager {
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByName(String name) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||
public ComputerDetails getComputerByUUID(String uuid) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
@@ -154,13 +158,6 @@ public class ComputerDatabaseManager {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
c.close();
|
||||
|
||||
// If a field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
||||
details.macAddress == null) {
|
||||
deleteComputer(details.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,40 @@ package com.limelight.computers;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.discovery.DiscoveryService;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
import com.limelight.utils.NetHelper;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
@@ -53,6 +62,7 @@ public class ComputerManagerService extends Service {
|
||||
private ComputerManagerListener listener = null;
|
||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private boolean pollingActive = false;
|
||||
private final Lock defaultNetworkLock = new ReentrantLock();
|
||||
|
||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||
@@ -95,7 +105,6 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
details.state = ComputerDetails.State.OFFLINE;
|
||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
releaseLocalDatabaseReference();
|
||||
@@ -106,17 +115,27 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
if (!newPc) {
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (dbManager.getComputerByName(details.name) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
||||
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (!newPc && existingComputer == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
|
||||
dbManager.updateComputer(details);
|
||||
// If we already have an entry for this computer in the DB, we must
|
||||
// combine the existing data with this new data (which may be partially available
|
||||
// due to detecting the PC via mDNS) without the saved external address. If we
|
||||
// write to the DB without doing this first, we can overwrite our existing data.
|
||||
if (existingComputer != null) {
|
||||
existingComputer.update(details);
|
||||
dbManager.updateComputer(existingComputer);
|
||||
}
|
||||
else {
|
||||
dbManager.updateComputer(details);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't call the listener if this is a failed lookup of a new PC
|
||||
@@ -156,7 +175,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setName("Polling thread for " + tuple.computer.localAddress);
|
||||
t.setName("Polling thread for " + tuple.computer.name);
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -177,7 +196,6 @@ public class ComputerManagerService extends Service {
|
||||
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;
|
||||
}
|
||||
|
||||
// Report this computer initially
|
||||
@@ -212,12 +230,12 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
||||
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
ComputerManagerService.this.removeComputer(name);
|
||||
public void removeComputer(ComputerDetails computer) {
|
||||
ComputerManagerService.this.removeComputer(computer);
|
||||
}
|
||||
|
||||
public void stopPolling() {
|
||||
@@ -233,7 +251,7 @@ public class ComputerManagerService extends Service {
|
||||
return idManager.getUniqueId();
|
||||
}
|
||||
|
||||
public ComputerDetails getComputer(UUID uuid) {
|
||||
public ComputerDetails getComputer(String uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
@@ -245,7 +263,7 @@ public class ComputerManagerService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void invalidateStateForComputer(UUID uuid) {
|
||||
public void invalidateStateForComputer(String uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
@@ -253,7 +271,6 @@ public class ComputerManagerService extends Service {
|
||||
// from wiping this change out
|
||||
synchronized (tuple.networkLock) {
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,12 +303,91 @@ public class ComputerManagerService extends Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void populateExternalAddress(ComputerDetails details) {
|
||||
boolean boundToNetwork = false;
|
||||
boolean activeNetworkIsVpn = NetHelper.isActiveNetworkVpn(this);
|
||||
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
|
||||
// Check if we're currently connected to a VPN which may send our
|
||||
// STUN request from an unexpected interface
|
||||
if (activeNetworkIsVpn) {
|
||||
// Acquire the default network lock since we could be changing global process state
|
||||
defaultNetworkLock.lock();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// On Lollipop or later, we can bind our process to the underlying interface
|
||||
// to ensure our STUN request goes out on that interface or not at all (which is
|
||||
// preferable to getting a VPN endpoint address back).
|
||||
Network[] networks = connMgr.getAllNetworks();
|
||||
for (Network net : networks) {
|
||||
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(net);
|
||||
if (netCaps != null) {
|
||||
if (!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||
!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
// This network looks like an underlying multicast-capable transport,
|
||||
// so let's guess that it's probably where our mDNS response came from.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (connMgr.bindProcessToNetwork(net)) {
|
||||
boundToNetwork = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (ConnectivityManager.setProcessDefaultNetwork(net)) {
|
||||
boundToNetwork = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the STUN request if we're not on a VPN or if we bound to a network
|
||||
if (!activeNetworkIsVpn || boundToNetwork) {
|
||||
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
}
|
||||
|
||||
// Unbind from the network
|
||||
if (boundToNetwork) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
connMgr.bindProcessToNetwork(null);
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
ConnectivityManager.setProcessDefaultNetwork(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock the network state
|
||||
if (activeNetworkIsVpn) {
|
||||
defaultNetworkLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private MdnsDiscoveryListener createDiscoveryListener() {
|
||||
return new MdnsDiscoveryListener() {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
// Populate the computer template with mDNS info
|
||||
if (computer.getLocalAddress() != null) {
|
||||
details.localAddress = computer.getLocalAddress().getHostAddress();
|
||||
|
||||
// Since we're on the same network, we can use STUN to find
|
||||
// our WAN address, which is also very likely the WAN address
|
||||
// of the PC. We can use this later to connect remotely.
|
||||
if (computer.getLocalAddress() instanceof Inet4Address) {
|
||||
populateExternalAddress(details);
|
||||
}
|
||||
}
|
||||
if (computer.getIpv6Address() != null) {
|
||||
details.ipv6Address = computer.getIpv6Address().getHostAddress();
|
||||
}
|
||||
|
||||
// Kick off a serverinfo poll on this machine
|
||||
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||
if (!addComputerBlocking(details)) {
|
||||
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -307,22 +403,13 @@ public class ComputerManagerService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
|
||||
private void addTuple(ComputerDetails details) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Check if this is the same computer
|
||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||
if (manuallyAdded) {
|
||||
// Update details anyway in case this machine has been re-added by IP
|
||||
// after not being reachable by our existing information
|
||||
tuple.computer.localAddress = details.localAddress;
|
||||
tuple.computer.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
else {
|
||||
// This indicates that mDNS discovered this address, so we
|
||||
// should only apply the local address.
|
||||
tuple.computer.localAddress = details.localAddress;
|
||||
}
|
||||
// Update the saved computer with potentially new details
|
||||
tuple.computer.update(details);
|
||||
|
||||
// Start a polling thread if polling is active
|
||||
if (pollingActive && tuple.thread == null) {
|
||||
@@ -347,15 +434,26 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localAddress = addr;
|
||||
fakeDetails.remoteAddress = addr;
|
||||
|
||||
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||
// Block while we try to fill the details
|
||||
try {
|
||||
runPoll(fakeDetails, true, 0);
|
||||
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
||||
// in the database, which would be bad because we don't have our pinned cert loaded yet.
|
||||
if (pollComputer(fakeDetails)) {
|
||||
// See if we have record of this PC to pull its pinned cert
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
|
||||
fakeDetails.serverCert = tuple.computer.serverCert;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll again, possibly with the pinned cert, to get accurate pairing information.
|
||||
// This will insert the host into the database too.
|
||||
runPoll(fakeDetails, true, 0);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -365,30 +463,26 @@ public class ComputerManagerService extends Service {
|
||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||
|
||||
// Start a polling thread for this machine
|
||||
addTuple(fakeDetails, manuallyAdded);
|
||||
addTuple(fakeDetails);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (!manuallyAdded) {
|
||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
public void removeComputer(ComputerDetails computer) {
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it from the database
|
||||
dbManager.deleteComputer(name);
|
||||
dbManager.deleteComputer(computer);
|
||||
|
||||
synchronized (pollingTuples) {
|
||||
// Remove the computer from the computer list
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.computer.name.equals(name)) {
|
||||
if (tuple.computer.uuid.equals(computer.uuid)) {
|
||||
if (tuple.thread != null) {
|
||||
// Interrupt the thread on this entry
|
||||
tuple.thread.interrupt();
|
||||
@@ -425,21 +519,29 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails();
|
||||
|
||||
// Check if this is the PC we expected
|
||||
if (details.uuid != null && newDetails.uuid != null &&
|
||||
!details.uuid.equals(newDetails.uuid)) {
|
||||
if (newDetails.uuid == null) {
|
||||
LimeLog.severe("Polling returned no UUID!");
|
||||
return null;
|
||||
}
|
||||
// details.uuid can be null on initial PC add
|
||||
else if (details.uuid != null && !details.uuid.equals(newDetails.uuid)) {
|
||||
// We got the wrong PC!
|
||||
LimeLog.info("Polling returned the wrong PC!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the new active address
|
||||
newDetails.activeAddress = address;
|
||||
|
||||
return newDetails;
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -447,6 +549,11 @@ public class ComputerManagerService extends Service {
|
||||
// Just try to establish a TCP connection to speculatively detect a running
|
||||
// GFE server
|
||||
private boolean fastPollIp(String address) {
|
||||
if (address == null) {
|
||||
// Don't bother if our address is null
|
||||
return false;
|
||||
}
|
||||
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||
@@ -475,12 +582,16 @@ public class ComputerManagerService extends Service {
|
||||
t.start();
|
||||
}
|
||||
|
||||
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException {
|
||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException {
|
||||
final boolean[] remoteInfo = new boolean[2];
|
||||
final boolean[] localInfo = new boolean[2];
|
||||
final boolean[] manualInfo = new boolean[2];
|
||||
final boolean[] ipv6Info = new boolean[2];
|
||||
|
||||
startFastPollThread(localAddress, localInfo);
|
||||
startFastPollThread(remoteAddress, remoteInfo);
|
||||
startFastPollThread(manualAddress, manualInfo);
|
||||
startFastPollThread(ipv6Address, ipv6Info);
|
||||
|
||||
// Check local first
|
||||
synchronized (localInfo) {
|
||||
@@ -489,174 +600,90 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
if (localInfo[1]) {
|
||||
return ComputerDetails.Reachability.LOCAL;
|
||||
return localAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remote
|
||||
// Now manual
|
||||
synchronized (manualInfo) {
|
||||
while (!manualInfo[0]) {
|
||||
manualInfo.wait(500);
|
||||
}
|
||||
|
||||
if (manualInfo[1]) {
|
||||
return manualAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remote IPv4
|
||||
synchronized (remoteInfo) {
|
||||
while (!remoteInfo[0]) {
|
||||
remoteInfo.wait(500);
|
||||
}
|
||||
|
||||
if (remoteInfo[1]) {
|
||||
return ComputerDetails.Reachability.REMOTE;
|
||||
return remoteAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
// Now global IPv6
|
||||
synchronized (ipv6Info) {
|
||||
while (!ipv6Info[0]) {
|
||||
ipv6Info.wait(500);
|
||||
}
|
||||
|
||||
private static boolean isAddressLikelyLocal(String str) {
|
||||
try {
|
||||
// This will tend to be wrong for IPv6 but falling back to
|
||||
// remote will be fine in that case. For IPv4, it should be
|
||||
// pretty accurate due to NAT prevalence.
|
||||
InetAddress addr = InetAddress.getByName(str);
|
||||
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||
ComputerDetails polledDetails;
|
||||
ComputerDetails.Reachability reachability;
|
||||
|
||||
if (details.localAddress.equals(details.remoteAddress)) {
|
||||
reachability = isAddressLikelyLocal(details.localAddress) ?
|
||||
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else {
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
|
||||
reachability = fastPollPc(details.localAddress, details.remoteAddress);
|
||||
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;
|
||||
if (ipv6Info[1]) {
|
||||
return ipv6Address;
|
||||
}
|
||||
}
|
||||
|
||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||
|
||||
if (localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localAddress);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||
}
|
||||
|
||||
String reachableAddr = null;
|
||||
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
|
||||
// Failed, so let's try the fallback
|
||||
if (!localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localAddress);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteAddress);
|
||||
}
|
||||
|
||||
if (polledDetails != null) {
|
||||
// The fallback poll worked
|
||||
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
|
||||
}
|
||||
}
|
||||
else if (polledDetails != null) {
|
||||
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
|
||||
}
|
||||
|
||||
if (reachableAddr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If both addresses are the same, guess whether we're local based on
|
||||
// IP address heuristics.
|
||||
if (reachableAddr.equals(polledDetails.localAddress) &&
|
||||
reachableAddr.equals(polledDetails.remoteAddress)) {
|
||||
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
|
||||
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else if (polledDetails.remoteAddress.equals(reachableAddr)) {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else if (polledDetails.localAddress.equals(reachableAddr)) {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
|
||||
return new ReachabilityTuple(polledDetails, reachableAddr);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
||||
if (initialReachTuple == null) {
|
||||
ComputerDetails polledDetails;
|
||||
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening.
|
||||
// Do not write this address to details.activeAddress because:
|
||||
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
||||
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
|
||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
if (candidateAddress == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
// Neither IP address reported in the serverinfo response was the one we used.
|
||||
// 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.localAddress = 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;
|
||||
// Try using the active address from fast-poll
|
||||
polledDetails = tryPollIp(details, candidateAddress);
|
||||
if (polledDetails == null) {
|
||||
// If that failed, try all unique addresses except what we've
|
||||
// already tried
|
||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||
uniqueAddresses.add(details.localAddress);
|
||||
uniqueAddresses.add(details.manualAddress);
|
||||
uniqueAddresses.add(details.remoteAddress);
|
||||
uniqueAddresses.add(details.ipv6Address);
|
||||
for (String addr : uniqueAddresses) {
|
||||
if (addr == null || addr.equals(candidateAddress)) {
|
||||
continue;
|
||||
}
|
||||
polledDetails = tryPollIp(details, addr);
|
||||
if (polledDetails != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save some details about the old state of the PC that we may wish
|
||||
// to restore later.
|
||||
String savedMacAddress = details.macAddress;
|
||||
String savedLocalAddress = details.localAddress;
|
||||
String savedRemoteAddress = details.remoteAddress;
|
||||
|
||||
// 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;
|
||||
if (polledDetails != null) {
|
||||
details.update(polledDetails);
|
||||
return true;
|
||||
}
|
||||
|
||||
// We never want to lose IP addresses by polling server info. If we get a poll back
|
||||
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
|
||||
// then we've lost an address in the polling and we should restore the one that's missing.
|
||||
if (details.localAddress.equals(details.remoteAddress) &&
|
||||
!savedLocalAddress.equals(savedRemoteAddress)) {
|
||||
if (details.localAddress.equals(savedLocalAddress)) {
|
||||
// Local addresses are identical, so put the old remote address back
|
||||
details.remoteAddress = savedRemoteAddress;
|
||||
}
|
||||
else if (details.remoteAddress.equals(savedRemoteAddress)) {
|
||||
// Remote addresses are identical, so put the old local address back
|
||||
details.localAddress = savedLocalAddress;
|
||||
}
|
||||
else {
|
||||
// Neither IP address match. Let's restore the remote address to be safe.
|
||||
details.remoteAddress = savedRemoteAddress;
|
||||
}
|
||||
|
||||
// Now update the reachability so the correct address is used
|
||||
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
|
||||
details.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
details.reachability = ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -679,7 +706,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||
// Add tuples for each computer
|
||||
addTuple(computer, true);
|
||||
addTuple(computer);
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
@@ -757,8 +784,9 @@ public class ComputerManagerService extends Service {
|
||||
public void run() {
|
||||
int emptyAppListResponses = 0;
|
||||
do {
|
||||
// Can't poll if it's not online
|
||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||
// Can't poll if it's not online or paired
|
||||
if (computer.state != ComputerDetails.State.ONLINE ||
|
||||
computer.pairState != PairingManager.PairState.PAIRED) {
|
||||
if (listener != null) {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
@@ -774,7 +802,7 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
String appList;
|
||||
if (tuple != null) {
|
||||
@@ -798,12 +826,12 @@ public class ComputerManagerService extends Service {
|
||||
// in a row, we'll go ahead and believe it.
|
||||
emptyAppListResponses++;
|
||||
}
|
||||
if (appList != null && !appList.isEmpty() &&
|
||||
if (!appList.isEmpty() &&
|
||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||
// Open the cache file
|
||||
OutputStream cacheOut = null;
|
||||
try {
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -830,7 +858,7 @@ public class ComputerManagerService extends Service {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
}
|
||||
else if (appList == null || appList.isEmpty()) {
|
||||
else if (appList.isEmpty()) {
|
||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -841,7 +869,7 @@ public class ComputerManagerService extends Service {
|
||||
} while (waitPollingDelay());
|
||||
}
|
||||
};
|
||||
thread.setName("App list polling thread for " + computer.localAddress);
|
||||
thread.setName("App list polling thread for " + computer.name);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class LegacyDatabaseReader {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
|
||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||
|
||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
details.uuid = c.getString(1);
|
||||
|
||||
// An earlier schema defined addresses as byte blobs. We'll
|
||||
// gracefully migrate those to strings so we can store DNS names
|
||||
// too. To disambiguate, we'll need to prefix them with a string
|
||||
// greater than the allowable IP address length.
|
||||
try {
|
||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy local address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(2);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted local address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy remote address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(3);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted remote address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
// On older versions of Moonlight, this is typically where manual addresses got stored,
|
||||
// so let's initialize it just to be safe.
|
||||
details.manualAddress = details.remoteAddress;
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
|
||||
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||
SQLiteDatabase computerDb = null;
|
||||
try {
|
||||
// Open the existing database
|
||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
return getAllComputers(computerDb);
|
||||
} catch (SQLiteException e) {
|
||||
return new LinkedList<ComputerDetails>();
|
||||
} finally {
|
||||
// Close and delete the old DB
|
||||
if (computerDb != null) {
|
||||
computerDb.close();
|
||||
}
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class LegacyDatabaseReader2 {
|
||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
|
||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
// This column wasn't always present in the old schema
|
||||
if (c.getColumnCount() >= 7) {
|
||||
try {
|
||||
byte[] derCertData = c.getBlob(6);
|
||||
|
||||
if (derCertData != null) {
|
||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||
SQLiteDatabase computerDb = null;
|
||||
try {
|
||||
// Open the existing database
|
||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
return getAllComputers(computerDb);
|
||||
} catch (SQLiteException e) {
|
||||
return new LinkedList<ComputerDetails>();
|
||||
} finally {
|
||||
// Close and delete the old DB
|
||||
if (computerDb != null) {
|
||||
computerDb.close();
|
||||
}
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
@@ -13,6 +15,7 @@ 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 com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -23,15 +26,34 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
private static final int SMALL_WIDTH_DP = 100;
|
||||
private static final int LARGE_WIDTH_DP = 150;
|
||||
|
||||
private final CachedAppAssetLoader loader;
|
||||
private final ComputerDetails computer;
|
||||
private final String uniqueId;
|
||||
|
||||
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));
|
||||
private CachedAppAssetLoader loader;
|
||||
|
||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId) {
|
||||
super(context, getLayoutIdForPreferences(prefs));
|
||||
|
||||
this.computer = computer;
|
||||
this.uniqueId = uniqueId;
|
||||
|
||||
updateLayoutWithPreferences(context, prefs);
|
||||
}
|
||||
|
||||
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||
if (prefs.smallIconMode) {
|
||||
return R.layout.app_grid_item_small;
|
||||
}
|
||||
else {
|
||||
return R.layout.app_grid_item;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||
int dpi = context.getResources().getDisplayMetrics().densityDpi;
|
||||
int dp;
|
||||
|
||||
if (small) {
|
||||
if (prefs.smallIconMode) {
|
||||
dp = SMALL_WIDTH_DP;
|
||||
}
|
||||
else {
|
||||
@@ -45,10 +67,19 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
}
|
||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||
|
||||
if (loader != null) {
|
||||
// Cancel operations on the old loader
|
||||
cancelQueuedOperations();
|
||||
}
|
||||
|
||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(),
|
||||
new DiskAssetLoader(context));
|
||||
new DiskAssetLoader(context),
|
||||
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
|
||||
|
||||
// This will trigger the view to reload with the new layout
|
||||
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||
}
|
||||
|
||||
public void cancelQueuedOperations() {
|
||||
@@ -80,30 +111,17 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
||||
public void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, AppView.AppObject obj) {
|
||||
// Let the cached asset loader handle it
|
||||
loader.populateImageView(obj.app, imgView, prgView);
|
||||
return true;
|
||||
}
|
||||
loader.populateImageView(obj.app, imgView, txtView);
|
||||
|
||||
@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;
|
||||
overlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
overlayView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// No overlay
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,17 +15,26 @@ 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;
|
||||
private int layoutId;
|
||||
final ArrayList<T> itemList = new ArrayList<>();
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public GenericGridAdapter(Context context, int layoutId) {
|
||||
GenericGridAdapter(Context context, int layoutId) {
|
||||
this.context = context;
|
||||
this.layoutId = layoutId;
|
||||
|
||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
void setLayoutId(int layoutId) {
|
||||
if (layoutId != this.layoutId) {
|
||||
this.layoutId = layoutId;
|
||||
|
||||
// Force the view to be redrawn with the new layout
|
||||
notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
itemList.clear();
|
||||
}
|
||||
@@ -45,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
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);
|
||||
public abstract void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, T obj);
|
||||
|
||||
@Override
|
||||
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||
@@ -60,22 +67,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||
ProgressBar prgView = 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);
|
||||
}
|
||||
}
|
||||
populateView(imgView, prgView, txtView, overlayView, itemList.get(i));
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@@ -9,14 +9,25 @@ import android.widget.TextView;
|
||||
import com.limelight.PcView;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
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 PcGridAdapter(Context context, PreferenceConfiguration prefs) {
|
||||
super(context, getLayoutIdForPreferences(prefs));
|
||||
}
|
||||
|
||||
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||
return R.layout.pc_grid_item;
|
||||
}
|
||||
|
||||
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||
// This will trigger the view to reload with the new layout
|
||||
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||
}
|
||||
|
||||
public void addComputer(PcView.ComputerObject computer) {
|
||||
@@ -38,7 +49,8 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
||||
public void populateView(ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, PcView.ComputerObject obj) {
|
||||
imgView.setImageResource(R.drawable.ic_computer);
|
||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||
imgView.setAlpha(1.0f);
|
||||
}
|
||||
@@ -46,19 +58,14 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
imgView.setAlpha(0.4f);
|
||||
}
|
||||
|
||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
if (obj.details.state == ComputerDetails.State.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) {
|
||||
txtView.setText(obj.details.name);
|
||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||
txtView.setAlpha(1.0f);
|
||||
}
|
||||
@@ -66,17 +73,21 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
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;
|
||||
overlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// We must check if the status is exactly online and unpaired
|
||||
// to avoid colliding with the loading spinner when status is unknown
|
||||
else if (obj.details.state == ComputerDetails.State.ONLINE &&
|
||||
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
||||
overlayView.setImageResource(R.drawable.ic_lock);
|
||||
overlayView.setAlpha(1.0f);
|
||||
overlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
overlayView.setVisibility(View.GONE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
@@ -52,15 +55,17 @@ public class CachedAppAssetLoader {
|
||||
private final MemoryAssetLoader memoryLoader;
|
||||
private final DiskAssetLoader diskLoader;
|
||||
private final Bitmap placeholderBitmap;
|
||||
private final Bitmap noAppImageBitmap;
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader) {
|
||||
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
|
||||
this.computer = computer;
|
||||
this.scalingDivider = scalingDivider;
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
this.noAppImageBitmap = noAppImageBitmap;
|
||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
@@ -87,7 +92,7 @@ public class CachedAppAssetLoader {
|
||||
memoryLoader.clearCache();
|
||||
}
|
||||
|
||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||
private ScaledBitmap 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
|
||||
@@ -108,7 +113,7 @@ public class CachedAppAssetLoader {
|
||||
// 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);
|
||||
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp != null) {
|
||||
return bmp;
|
||||
}
|
||||
@@ -130,29 +135,29 @@ public class CachedAppAssetLoader {
|
||||
return null;
|
||||
}
|
||||
|
||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, ScaledBitmap> {
|
||||
private final WeakReference<ImageView> imageViewRef;
|
||||
private final WeakReference<ProgressBar> progressViewRef;
|
||||
private final WeakReference<TextView> textViewRef;
|
||||
private final boolean diskOnly;
|
||||
|
||||
private LoaderTuple tuple;
|
||||
|
||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
||||
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
|
||||
this.imageViewRef = new WeakReference<>(imageView);
|
||||
this.progressViewRef = new WeakReference<>(prgView);
|
||||
this.textViewRef = new WeakReference<>(textView);
|
||||
this.diskOnly = diskOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||
protected ScaledBitmap 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) {
|
||||
if (isCancelled() || imageViewRef.get() == null || textViewRef.get() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||
if (bmp == null) {
|
||||
if (!diskOnly) {
|
||||
// Try to load the asset from the network
|
||||
@@ -181,44 +186,61 @@ public class CachedAppAssetLoader {
|
||||
|
||||
// If the current loader task for this view isn't us, do nothing
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
final ProgressBar prgView = progressViewRef.get();
|
||||
final TextView textView = textViewRef.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);
|
||||
// Set off another loader task on the network executor. This time our AsyncDrawable
|
||||
// will use the app image placeholder bitmap, rather than an empty bitmap.
|
||||
LoaderTask task = new LoaderTask(imageView, textView, false);
|
||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
task.executeOnExecutor(networkExecutor, tuple);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
protected void onPostExecute(final ScaledBitmap bitmap) {
|
||||
// Do nothing if cancelled
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
final ProgressBar prgView = progressViewRef.get();
|
||||
final TextView textView = textViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Set the bitmap
|
||||
// Fade in the box art
|
||||
if (bitmap != null) {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
// Show the text if it's a placeholder
|
||||
textView.setVisibility(isBitmapPlaceholder(bitmap) ? View.VISIBLE : View.GONE);
|
||||
|
||||
// Hide the progress bar
|
||||
if (prgView != null) {
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (imageView.getVisibility() == View.VISIBLE) {
|
||||
// Fade out the placeholder first
|
||||
Animation fadeOutAnimation = AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadeout);
|
||||
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
|
||||
// Show the view
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
// Fade in the new box art
|
||||
imageView.setImageBitmap(bitmap.bitmap);
|
||||
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
imageView.startAnimation(fadeOutAnimation);
|
||||
}
|
||||
else {
|
||||
// View is invisible already, so just fade in the new art
|
||||
imageView.setImageBitmap(bitmap.bitmap);
|
||||
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +318,13 @@ public class CachedAppAssetLoader {
|
||||
});
|
||||
}
|
||||
|
||||
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
||||
private boolean isBitmapPlaceholder(ScaledBitmap bitmap) {
|
||||
return (bitmap == null) ||
|
||||
(bitmap.originalWidth == 130 && bitmap.originalHeight == 180) || // GFE 2.0
|
||||
(bitmap.originalWidth == 628 && bitmap.originalHeight == 888); // GFE 3.0
|
||||
}
|
||||
|
||||
public boolean populateImageView(NvApp app, ImageView imgView, TextView textView) {
|
||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||
|
||||
// If there's already a task in progress for this view,
|
||||
@@ -306,22 +334,26 @@ public class CachedAppAssetLoader {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hide the progress bar always on initial load
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
// Always set the name text so we have it if needed later
|
||||
textView.setText(app.getAppName());
|
||||
|
||||
// First, try the memory cache in the current context
|
||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||
ScaledBitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||
if (bmp != null) {
|
||||
// Show the bitmap immediately
|
||||
imgView.setVisibility(View.VISIBLE);
|
||||
imgView.setImageBitmap(bmp);
|
||||
imgView.setImageBitmap(bmp.bitmap);
|
||||
|
||||
// Show the text if it's a placeholder bitmap
|
||||
textView.setVisibility(isBitmapPlaceholder(bmp) ? View.VISIBLE : View.GONE);
|
||||
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 LoaderTask task = new LoaderTask(imgView, textView, true);
|
||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||
textView.setVisibility(View.INVISIBLE);
|
||||
imgView.setVisibility(View.INVISIBLE);
|
||||
imgView.setImageDrawable(asyncDrawable);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public class DiskAssetLoader {
|
||||
}
|
||||
|
||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
}
|
||||
|
||||
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
||||
@@ -64,8 +64,8 @@ public class DiskAssetLoader {
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||
|
||||
// Don't bother with anything if it doesn't exist
|
||||
if (!file.exists()) {
|
||||
@@ -110,34 +110,54 @@ public class DiskAssetLoader {
|
||||
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
if (bmp != null) {
|
||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
||||
return new ScaledBitmap(decodeOnlyOptions.outWidth, decodeOnlyOptions.outHeight, bmp);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// On P, we can get a bitmap back in one step with ImageDecoder
|
||||
final ScaledBitmap scaledBitmap = new ScaledBitmap();
|
||||
try {
|
||||
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||
scaledBitmap.bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||
@Override
|
||||
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
||||
scaledBitmap.originalWidth = imageInfo.getSize().getWidth();
|
||||
scaledBitmap.originalHeight = imageInfo.getSize().getHeight();
|
||||
|
||||
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
||||
if (isLowRamDevice) {
|
||||
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
||||
}
|
||||
}
|
||||
});
|
||||
return scaledBitmap;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return bmp;
|
||||
return null;
|
||||
}
|
||||
|
||||
public File getFile(String computerUuid, int appId) {
|
||||
return CacheHelper.openPath(false, cacheDir, "boxart", computerUuid, appId + ".png");
|
||||
}
|
||||
|
||||
public void deleteAssetsForComputer(String computerUuid) {
|
||||
File dir = CacheHelper.openPath(false, cacheDir, "boxart", computerUuid);
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
@@ -151,7 +171,7 @@ public class DiskAssetLoader {
|
||||
|
||||
if (!success) {
|
||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,74 @@
|
||||
package com.limelight.grid.assets;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.LruCache;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.HashMap;
|
||||
|
||||
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) {
|
||||
private static final LruCache<String, ScaledBitmap> memoryCache = new LruCache<String, ScaledBitmap>(maxMemory / 16) {
|
||||
@Override
|
||||
protected int sizeOf(String key, Bitmap bitmap) {
|
||||
protected int sizeOf(String key, ScaledBitmap bitmap) {
|
||||
// Sizeof returns kilobytes
|
||||
return bitmap.getByteCount() / 1024;
|
||||
return bitmap.bitmap.getByteCount() / 1024;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entryRemoved(boolean evicted, String key, ScaledBitmap oldValue, ScaledBitmap newValue) {
|
||||
super.entryRemoved(evicted, key, oldValue, newValue);
|
||||
|
||||
if (evicted) {
|
||||
// Keep a soft reference around to the bitmap as long as we can
|
||||
evictionCache.put(key, new SoftReference<>(oldValue));
|
||||
}
|
||||
}
|
||||
};
|
||||
private static final HashMap<String, SoftReference<ScaledBitmap>> evictionCache = new HashMap<>();
|
||||
|
||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
||||
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
final String key = constructKey(tuple);
|
||||
|
||||
ScaledBitmap bmp = memoryCache.get(key);
|
||||
if (bmp != null) {
|
||||
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
||||
LimeLog.info("LRU cache hit for tuple: "+tuple);
|
||||
return bmp;
|
||||
}
|
||||
return bmp;
|
||||
|
||||
SoftReference<ScaledBitmap> bmpRef = evictionCache.get(key);
|
||||
if (bmpRef != null) {
|
||||
bmp = bmpRef.get();
|
||||
if (bmp != null) {
|
||||
LimeLog.info("Eviction cache hit for tuple: "+tuple);
|
||||
|
||||
// Put this entry back into the LRU cache
|
||||
evictionCache.remove(key);
|
||||
memoryCache.put(key, bmp);
|
||||
|
||||
return bmp;
|
||||
}
|
||||
else {
|
||||
// The data is gone, so remove the dangling SoftReference now
|
||||
evictionCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, ScaledBitmap bitmap) {
|
||||
memoryCache.put(constructKey(tuple), bitmap);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
// We must evict first because that will push all items into the eviction cache
|
||||
memoryCache.evictAll();
|
||||
evictionCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ public class NetworkAssetLoader {
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
InputStream in = null;
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
||||
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.limelight.grid.assets;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
public class ScaledBitmap {
|
||||
public int originalWidth;
|
||||
public int originalHeight;
|
||||
|
||||
public Bitmap bitmap;
|
||||
|
||||
public ScaledBitmap() {}
|
||||
|
||||
public ScaledBitmap(int originalWidth, int originalHeight, Bitmap bitmap) {
|
||||
this.originalWidth = originalWidth;
|
||||
this.originalHeight = originalHeight;
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class ConnectionContext {
|
||||
public String serverAddress;
|
||||
public X509Certificate serverCert;
|
||||
public StreamConfiguration streamConfig;
|
||||
public NvConnectionListener connListener;
|
||||
public SecretKey riKey;
|
||||
public int riKeyId;
|
||||
|
||||
// This is the version quad from the appversion tag of /serverinfo
|
||||
public String serverAppVersion;
|
||||
public String serverGfeVersion;
|
||||
|
||||
public int negotiatedWidth, negotiatedHeight;
|
||||
public boolean negotiatedHdr;
|
||||
|
||||
public int videoCapabilities;
|
||||
}
|
||||
360
app/src/main/java/com/limelight/nvstream/NvConnection.java
Normal file
360
app/src/main/java/com/limelight/nvstream/NvConnection.java
Normal file
@@ -0,0 +1,360 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class NvConnection {
|
||||
// Context parameters
|
||||
private String host;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private String uniqueId;
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||
{
|
||||
this.host = host;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
|
||||
this.context = new ConnectionContext();
|
||||
this.context.streamConfig = config;
|
||||
this.context.serverCert = serverCert;
|
||||
try {
|
||||
// This is unique per connection
|
||||
this.context.riKey = generateRiAesKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
this.context.riKeyId = generateRiKeyId();
|
||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||
}
|
||||
|
||||
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||
|
||||
// RI keys are 128 bits
|
||||
keyGen.init(128);
|
||||
|
||||
return keyGen.generateKey();
|
||||
}
|
||||
|
||||
private static int generateRiKeyId() {
|
||||
return new SecureRandom().nextInt();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Interrupt any pending connection. This is thread-safe.
|
||||
MoonBridge.interruptConnection();
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.stopConnection();
|
||||
MoonBridge.cleanupBridge();
|
||||
}
|
||||
|
||||
// Now a pending connection can be processed
|
||||
connectionAllowed.release();
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||
|
||||
String serverInfo = h.getServerInfo();
|
||||
|
||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||
if (context.serverAppVersion == null) {
|
||||
context.connListener.displayMessage("Server version malformed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// May be missing for older servers
|
||||
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||
|
||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||
context.connListener.displayMessage("Device not paired with computer");
|
||||
return false;
|
||||
}
|
||||
|
||||
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||
context.negotiatedHdr = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Decide on negotiated stream parameters now
|
||||
//
|
||||
|
||||
// Check for a supported stream resolution
|
||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||
// Client wants 4K but the server can't do it
|
||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||
|
||||
// Lower resolution to 1080p
|
||||
context.negotiatedWidth = 1920;
|
||||
context.negotiatedHeight = 1080;
|
||||
}
|
||||
else {
|
||||
// Take what the client wanted
|
||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||
}
|
||||
|
||||
//
|
||||
// Video stream format will be decided during the RTSP handshake
|
||||
//
|
||||
|
||||
NvApp app = context.streamConfig.getApp();
|
||||
|
||||
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||
if (!context.streamConfig.getApp().isInitialized()) {
|
||||
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||
if (app == null) {
|
||||
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a game running, resume it
|
||||
if (h.getCurrentGame(serverInfo) != 0) {
|
||||
try {
|
||||
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||
if (!h.resumeApp(context)) {
|
||||
context.connListener.displayMessage("Failed to resume existing session");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return quitAndLaunch(h, context);
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 470) {
|
||||
// This is the error you get when you try to resume a session that's not yours.
|
||||
// Because this is fairly common, we'll display a more detailed message.
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be resumed. End streaming on the original " +
|
||||
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else if (e.getErrorCode() == 525) {
|
||||
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||
"quit the session and start streaming again.");
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Resumed existing game session");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||
XmlPullParserException {
|
||||
try {
|
||||
if (!h.quitApp()) {
|
||||
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||
return false;
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 599) {
|
||||
context.connListener.displayMessage("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()+")");
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
|
||||
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||
throws IOException, XmlPullParserException {
|
||||
// Launch the app since it's not running
|
||||
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||
context.connListener.displayMessage("Failed to launch application");
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Launched new game session");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
context.connListener = connectionListener;
|
||||
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||
|
||||
String appName = context.streamConfig.getApp().getAppName();
|
||||
|
||||
context.serverAddress = host;
|
||||
context.connListener.stageStarting(appName);
|
||||
|
||||
try {
|
||||
if (!startApp()) {
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
context.connListener.stageComplete(appName);
|
||||
} catch (GfeHttpResponseException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, e.getErrorCode());
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||
ib.putInt(context.riKeyId);
|
||||
|
||||
// Acquire the connection semaphore to ensure we only have one
|
||||
// connection going at once.
|
||||
try {
|
||||
connectionAllowed.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||
context.serverAppVersion, context.serverGfeVersion,
|
||||
context.negotiatedWidth, context.negotiatedHeight,
|
||||
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||
context.streamConfig.getMaxPacketSize(),
|
||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
|
||||
context.streamConfig.getHevcSupported(),
|
||||
context.negotiatedHdr,
|
||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||
context.streamConfig.getClientRefreshRateX100(),
|
||||
context.riKey.getEncoded(), ib.array(),
|
||||
context.videoCapabilities);
|
||||
if (ret != 0) {
|
||||
// LiStartConnection() failed, so the caller is not expected
|
||||
// to stop the connection themselves. We need to release their
|
||||
// semaphore count for them.
|
||||
connectionAllowed.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonUp(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short controllerNumber,
|
||||
final short activeGamepadMask, final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||
leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseScroll(final byte scrollClicks) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseScroll(scrollClicks);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseHighResScroll(final short scrollAmount) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseHighResScroll(scrollAmount);
|
||||
}
|
||||
}
|
||||
|
||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
public interface NvConnectionListener {
|
||||
void stageStarting(String stage);
|
||||
void stageComplete(String stage);
|
||||
void stageFailed(String stage, int errorCode);
|
||||
|
||||
void connectionStarted();
|
||||
void connectionTerminated(int errorCode);
|
||||
void connectionStatusUpdate(int connectionStatus);
|
||||
|
||||
void displayMessage(String message);
|
||||
void displayTransientMessage(String message);
|
||||
|
||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class StreamConfiguration {
|
||||
public static final int INVALID_APP_ID = 0;
|
||||
|
||||
public static final int STREAM_CFG_LOCAL = 0;
|
||||
public static final int STREAM_CFG_REMOTE = 1;
|
||||
public static final int STREAM_CFG_AUTO = 2;
|
||||
|
||||
private NvApp app;
|
||||
private int width, height;
|
||||
private int refreshRate;
|
||||
private int launchRefreshRate;
|
||||
private int clientRefreshRateX100;
|
||||
private int bitrate;
|
||||
private boolean sops;
|
||||
private boolean enableAdaptiveResolution;
|
||||
private boolean playLocalAudio;
|
||||
private int maxPacketSize;
|
||||
private int remote;
|
||||
private MoonBridge.AudioConfiguration audioConfiguration;
|
||||
private boolean supportsHevc;
|
||||
private int hevcBitratePercentageMultiplier;
|
||||
private boolean enableHdr;
|
||||
private int attachedGamepadMask;
|
||||
|
||||
public static class Builder {
|
||||
private StreamConfiguration config = new StreamConfiguration();
|
||||
|
||||
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||
config.app = app;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||
config.remote = remote;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||
config.refreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
|
||||
config.launchRefreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||
config.bitrate = bitrate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||
config.sops = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||
config.enableAdaptiveResolution = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||
config.playLocalAudio = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||
config.maxPacketSize = maxPacketSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||
config.hevcBitratePercentageMultiplier = multiplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||
config.enableHdr = enableHdr;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||
config.attachedGamepadMask = attachedGamepadMask;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||
config.attachedGamepadMask = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (gamepadCount > i) {
|
||||
config.attachedGamepadMask |= 1 << i;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||
config.clientRefreshRateX100 = refreshRateX100;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
|
||||
config.audioConfiguration = audioConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||
config.supportsHevc = supportsHevc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration build() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamConfiguration() {
|
||||
// Set default attributes
|
||||
this.app = new NvApp("Steam");
|
||||
this.width = 1280;
|
||||
this.height = 720;
|
||||
this.refreshRate = 60;
|
||||
this.launchRefreshRate = 60;
|
||||
this.bitrate = 10000;
|
||||
this.maxPacketSize = 1024;
|
||||
this.remote = STREAM_CFG_AUTO;
|
||||
this.sops = true;
|
||||
this.enableAdaptiveResolution = false;
|
||||
this.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
|
||||
this.supportsHevc = false;
|
||||
this.enableHdr = false;
|
||||
this.attachedGamepadMask = 0;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getRefreshRate() {
|
||||
return refreshRate;
|
||||
}
|
||||
|
||||
public int getLaunchRefreshRate() {
|
||||
return launchRefreshRate;
|
||||
}
|
||||
|
||||
public int getBitrate() {
|
||||
return bitrate;
|
||||
}
|
||||
|
||||
public int getMaxPacketSize() {
|
||||
return maxPacketSize;
|
||||
}
|
||||
|
||||
public NvApp getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public boolean getSops() {
|
||||
return sops;
|
||||
}
|
||||
|
||||
public boolean getAdaptiveResolutionEnabled() {
|
||||
return enableAdaptiveResolution;
|
||||
}
|
||||
|
||||
public boolean getPlayLocalAudio() {
|
||||
return playLocalAudio;
|
||||
}
|
||||
|
||||
public int getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public MoonBridge.AudioConfiguration getAudioConfiguration() {
|
||||
return audioConfiguration;
|
||||
}
|
||||
|
||||
public boolean getHevcSupported() {
|
||||
return supportsHevc;
|
||||
}
|
||||
|
||||
public int getHevcBitratePercentageMultiplier() {
|
||||
return hevcBitratePercentageMultiplier;
|
||||
}
|
||||
|
||||
public boolean getEnableHdr() {
|
||||
return enableHdr;
|
||||
}
|
||||
|
||||
public int getAttachedGamepadMask() {
|
||||
return attachedGamepadMask;
|
||||
}
|
||||
|
||||
public int getClientRefreshRateX100() {
|
||||
return clientRefreshRateX100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class ByteBufferDescriptor {
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public ByteBufferDescriptor nextDescriptor;
|
||||
|
||||
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||
{
|
||||
this.data = desc.data;
|
||||
this.offset = desc.offset;
|
||||
this.length = desc.length;
|
||||
}
|
||||
|
||||
public void reinitialize(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.nextDescriptor = null;
|
||||
}
|
||||
|
||||
public void print()
|
||||
{
|
||||
print(offset, length);
|
||||
}
|
||||
|
||||
public void print(int length)
|
||||
{
|
||||
print(this.offset, length);
|
||||
}
|
||||
|
||||
public void print(int offset, int length)
|
||||
{
|
||||
for (int i = offset; i < offset+length;) {
|
||||
if (i + 8 <= offset+length) {
|
||||
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||
i += 8;
|
||||
}
|
||||
else {
|
||||
System.out.printf("%x: %02x \n", i, data[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.limelight.nvstream.av.audio;
|
||||
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public interface AudioRenderer {
|
||||
int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame);
|
||||
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
|
||||
void playDecodedAudio(short[] audioData);
|
||||
|
||||
void cleanup();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
|
||||
public abstract class VideoDecoderRenderer {
|
||||
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||
|
||||
public abstract void start();
|
||||
|
||||
public abstract void stop();
|
||||
|
||||
// This is called once for each frame-start NALU. This means it will be called several times
|
||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||
int frameNumber, long receiveTimeMs);
|
||||
|
||||
public abstract void cleanup();
|
||||
|
||||
public abstract int getCapabilities();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
|
||||
public class ComputerDetails {
|
||||
public enum State {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
|
||||
// Persistent attributes
|
||||
public String uuid;
|
||||
public String name;
|
||||
public String localAddress;
|
||||
public String remoteAddress;
|
||||
public String manualAddress;
|
||||
public String ipv6Address;
|
||||
public String macAddress;
|
||||
public X509Certificate serverCert;
|
||||
|
||||
// Transient attributes
|
||||
public State state;
|
||||
public String activeAddress;
|
||||
public PairingManager.PairState pairState;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
|
||||
public ComputerDetails() {
|
||||
// Use defaults
|
||||
state = State.UNKNOWN;
|
||||
}
|
||||
|
||||
public ComputerDetails(ComputerDetails details) {
|
||||
// Copy details from the other computer
|
||||
update(details);
|
||||
}
|
||||
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.name = details.name;
|
||||
this.uuid = details.uuid;
|
||||
if (details.activeAddress != null) {
|
||||
this.activeAddress = details.activeAddress;
|
||||
}
|
||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||
this.localAddress = details.localAddress;
|
||||
}
|
||||
if (details.remoteAddress != null) {
|
||||
this.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
if (details.manualAddress != null) {
|
||||
this.manualAddress = details.manualAddress;
|
||||
}
|
||||
if (details.ipv6Address != null) {
|
||||
this.ipv6Address = details.ipv6Address;
|
||||
}
|
||||
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||
this.macAddress = details.macAddress;
|
||||
}
|
||||
if (details.serverCert != null) {
|
||||
this.serverCert = details.serverCert;
|
||||
}
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("Name: ").append(name).append("\n");
|
||||
str.append("State: ").append(state).append("\n");
|
||||
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||
str.append("UUID: ").append(uuid).append("\n");
|
||||
str.append("Local Address: ").append(localAddress).append("\n");
|
||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GfeHttpResponseException extends IOException {
|
||||
private static final long serialVersionUID = 1543508830807804222L;
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
|
||||
public interface LimelightCryptoProvider {
|
||||
X509Certificate getClientCertificate();
|
||||
RSAPrivateKey getClientPrivateKey();
|
||||
byte[] getPemEncodedClientCertificate();
|
||||
String encodeBase64String(byte[] data);
|
||||
}
|
||||
70
app/src/main/java/com/limelight/nvstream/http/NvApp.java
Normal file
70
app/src/main/java/com/limelight/nvstream/http/NvApp.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class NvApp {
|
||||
private String appName = "";
|
||||
private int appId;
|
||||
private boolean initialized;
|
||||
private boolean hdrSupported;
|
||||
|
||||
public NvApp() {}
|
||||
|
||||
public NvApp(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.hdrSupported = hdrSupported;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
try {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
this.initialized = true;
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed app ID: "+appId);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppId(int appId) {
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setHdrSupported(boolean hdrSupported) {
|
||||
this.hdrSupported = hdrSupported;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public boolean isHdrSupported() {
|
||||
return this.hdrSupported;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("Name: ").append(appName).append("\n");
|
||||
str.append("HDR: ").append(hdrSupported ? "Yes" : "No").append("\n");
|
||||
str.append("ID: ").append(appId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
698
app/src/main/java/com/limelight/nvstream/http/NvHTTP.java
Normal file
698
app/src/main/java/com/limelight/nvstream/http/NvHTTP.java
Normal file
@@ -0,0 +1,698 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Stack;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509KeyManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import com.limelight.BuildConfig;
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.ConnectionContext;
|
||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.Handshake;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
|
||||
public class NvHTTP {
|
||||
private String uniqueId;
|
||||
private PairingManager pm;
|
||||
|
||||
public static final int HTTPS_PORT = 47984;
|
||||
public static final int HTTP_PORT = 47989;
|
||||
public static final int CONNECTION_TIMEOUT = 3000;
|
||||
public static final int READ_TIMEOUT = 5000;
|
||||
|
||||
// Print URL and content to logcat on debug builds
|
||||
private static boolean verbose = BuildConfig.DEBUG;
|
||||
|
||||
public String baseUrlHttps;
|
||||
public String baseUrlHttp;
|
||||
|
||||
private OkHttpClient httpClient;
|
||||
private OkHttpClient httpClientWithReadTimeout;
|
||||
|
||||
private X509TrustManager trustManager;
|
||||
private X509KeyManager keyManager;
|
||||
private X509Certificate serverCert;
|
||||
|
||||
void setServerCert(X509Certificate serverCert) {
|
||||
this.serverCert = serverCert;
|
||||
|
||||
trustManager = new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||
throw new IllegalStateException("Should never be called");
|
||||
}
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
// Check the server certificate if we've paired to this host
|
||||
if (!certs[0].equals(NvHTTP.this.serverCert)) {
|
||||
throw new CertificateException("Certificate mismatch");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
|
||||
// Set up TrustManager
|
||||
setServerCert(serverCert);
|
||||
|
||||
keyManager = new X509KeyManager() {
|
||||
public String chooseClientAlias(String[] keyTypes,
|
||||
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||
Socket socket) { return null; }
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||
}
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) { return null; }
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
|
||||
};
|
||||
|
||||
// Ignore differences between given hostname and certificate hostname
|
||||
HostnameVerifier hv = new HostnameVerifier() {
|
||||
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||
};
|
||||
|
||||
httpClient = new OkHttpClient.Builder()
|
||||
.connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
|
||||
.hostnameVerifier(hv)
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
httpClientWithReadTimeout = httpClient.newBuilder()
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException {
|
||||
// Use the same UID for all Moonlight clients so we can quit games
|
||||
// started by other Moonlight clients.
|
||||
this.uniqueId = "0123456789ABCDEF";
|
||||
|
||||
initializeHttpState(serverCert, cryptoProvider);
|
||||
|
||||
try {
|
||||
// The URI constructor takes care of escaping IPv6 literals
|
||||
this.baseUrlHttps = new URI("https", null, address, HTTPS_PORT, null, null, null).toString();
|
||||
this.baseUrlHttp = new URI("http", null, address, HTTP_PORT, null, null, null).toString();
|
||||
} catch (URISyntaxException e) {
|
||||
// Encapsulate URISyntaxException into IOException for callers to handle more easily
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
this.pm = new PairingManager(this, cryptoProvider);
|
||||
}
|
||||
|
||||
String buildUniqueIdUuidString() {
|
||||
return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID();
|
||||
}
|
||||
|
||||
static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException {
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
|
||||
xpp.setInput(r);
|
||||
int eventType = xpp.getEventType();
|
||||
Stack<String> currentTag = new Stack<String>();
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case (XmlPullParser.START_TAG):
|
||||
if (xpp.getName().equals("root")) {
|
||||
verifyResponseStatus(xpp);
|
||||
}
|
||||
currentTag.push(xpp.getName());
|
||||
break;
|
||||
case (XmlPullParser.END_TAG):
|
||||
currentTag.pop();
|
||||
break;
|
||||
case (XmlPullParser.TEXT):
|
||||
if (currentTag.peek().equals(tagname)) {
|
||||
return xpp.getText().trim();
|
||||
}
|
||||
break;
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException {
|
||||
return getXmlString(new StringReader(str), tagname);
|
||||
}
|
||||
|
||||
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
|
||||
// We use Long.parseLong() because in rare cases GFE can send back a status code of
|
||||
// 0xFFFFFFFF, which will cause Integer.parseInt() to throw a NumberFormatException due
|
||||
// to exceeding Integer.MAX_VALUE. We'll get the desired error code of -1 by just casting
|
||||
// the resulting long into an int.
|
||||
int statusCode = (int)Long.parseLong(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
|
||||
if (statusCode != 200) {
|
||||
String statusMsg = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message");
|
||||
if (statusCode == -1 && "Invalid".equals(statusMsg)) {
|
||||
// Special case handling an audio capture error which GFE doesn't
|
||||
// provide any useful status message for.
|
||||
statusCode = 418;
|
||||
statusMsg = "Missing audio capture device. Reinstall GeForce Experience.";
|
||||
}
|
||||
throw new GfeHttpResponseException(statusCode, statusMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public String getServerInfo() throws IOException, XmlPullParserException {
|
||||
String resp;
|
||||
|
||||
//
|
||||
// TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP.
|
||||
// For some reason, we always see PairStatus is 0 over HTTP and only 1 over HTTPS. It looks
|
||||
// like there are extra request headers required to make this stuff work over HTTP.
|
||||
//
|
||||
|
||||
// When we have a pinned cert, use HTTPS to fetch serverinfo and fall back on cert mismatch
|
||||
if (serverCert != null) {
|
||||
try {
|
||||
try {
|
||||
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
|
||||
} catch (SSLHandshakeException e) {
|
||||
// Detect if we failed due to a server cert mismatch
|
||||
if (e.getCause() instanceof CertificateException) {
|
||||
// Jump to the GfeHttpResponseException exception handler to retry
|
||||
// over HTTP which will allow us to pair again to update the cert
|
||||
throw new GfeHttpResponseException(401, "Server certificate mismatch");
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// This will throw an exception if the request came back with a failure status.
|
||||
// We want this because it will throw us into the HTTP case if the client is unpaired.
|
||||
getServerVersion(resp);
|
||||
}
|
||||
catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 401) {
|
||||
// Cert validation error - fall back to HTTP
|
||||
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
||||
}
|
||||
|
||||
// If it's not a cert validation error, throw it
|
||||
throw e;
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
else {
|
||||
// No pinned cert, so use HTTP
|
||||
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
||||
}
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
String serverInfo = getServerInfo();
|
||||
|
||||
details.name = getXmlString(serverInfo, "hostname");
|
||||
if (details.name == null || details.name.isEmpty()) {
|
||||
details.name = "UNKNOWN";
|
||||
}
|
||||
|
||||
details.uuid = getXmlString(serverInfo, "uniqueid");
|
||||
details.macAddress = getXmlString(serverInfo, "mac");
|
||||
details.localAddress = getXmlString(serverInfo, "LocalIP");
|
||||
|
||||
// This may be null, but that's okay
|
||||
details.remoteAddress = getXmlString(serverInfo, "ExternalIP");
|
||||
|
||||
// This has some extra logic to always report unpaired if the pinned cert isn't there
|
||||
details.pairState = getPairState(serverInfo);
|
||||
|
||||
try {
|
||||
details.runningGameId = getCurrentGame(serverInfo);
|
||||
} catch (NumberFormatException e) {
|
||||
details.runningGameId = 0;
|
||||
}
|
||||
|
||||
// We could reach it so it's online
|
||||
details.state = ComputerDetails.State.ONLINE;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
// This hack is Android-specific but we do it on all platforms
|
||||
// because it doesn't really matter
|
||||
private OkHttpClient performAndroidTlsHack(OkHttpClient client) {
|
||||
// Doing this each time we create a socket is required
|
||||
// to avoid the SSLv3 fallback that causes connection failures
|
||||
try {
|
||||
SSLContext sc = SSLContext.getInstance("TLS");
|
||||
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
|
||||
return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build();
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public X509Certificate getCertificateIfTrusted() {
|
||||
try {
|
||||
Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute();
|
||||
Handshake handshake = resp.handshake();
|
||||
if (handshake != null) {
|
||||
return (X509Certificate)handshake.peerCertificates().get(0);
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read timeout should be enabled for any HTTP query that requires no outside action
|
||||
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
|
||||
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
|
||||
// queries do not.
|
||||
private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException {
|
||||
Request request = new Request.Builder().url(url).get().build();
|
||||
Response response;
|
||||
|
||||
if (serverCert == null && !url.startsWith(baseUrlHttp)) {
|
||||
throw new IllegalStateException("Attempted HTTPS fetch without pinned cert");
|
||||
}
|
||||
|
||||
if (enableReadTimeout) {
|
||||
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
|
||||
}
|
||||
else {
|
||||
response = performAndroidTlsHack(httpClient).newCall(request).execute();
|
||||
}
|
||||
|
||||
ResponseBody body = response.body();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return body;
|
||||
}
|
||||
|
||||
// Unsuccessful, so close the response body
|
||||
if (body != null) {
|
||||
body.close();
|
||||
}
|
||||
|
||||
if (response.code() == 404) {
|
||||
throw new FileNotFoundException(url);
|
||||
}
|
||||
else {
|
||||
throw new GfeHttpResponseException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
|
||||
String openHttpConnectionToString(String url, boolean enableReadTimeout) throws IOException {
|
||||
try {
|
||||
if (verbose) {
|
||||
LimeLog.info("Requesting URL: "+url);
|
||||
}
|
||||
|
||||
ResponseBody resp = openHttpConnection(url, enableReadTimeout);
|
||||
String respString = resp.string();
|
||||
resp.close();
|
||||
|
||||
if (verbose) {
|
||||
LimeLog.info(url+" -> "+respString);
|
||||
}
|
||||
|
||||
return respString;
|
||||
} catch (IOException e) {
|
||||
if (verbose) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||
return getXmlString(serverInfo, "appversion");
|
||||
}
|
||||
|
||||
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
|
||||
return getPairState(getServerInfo());
|
||||
}
|
||||
|
||||
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
|
||||
// If we don't have a server cert, we can't be paired even if the host thinks we are
|
||||
if (serverCert == null) {
|
||||
return PairState.NOT_PAIRED;
|
||||
}
|
||||
|
||||
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
|
||||
return PairState.NOT_PAIRED;
|
||||
}
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
|
||||
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
|
||||
if (str != null) {
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
|
||||
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
if (str != null) {
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Possible meaning of bits
|
||||
// Bit 0: H.264 Baseline
|
||||
// Bit 1: H.264 High
|
||||
// ----
|
||||
// Bit 8: HEVC Main
|
||||
// Bit 9: HEVC Main10
|
||||
// Bit 10: HEVC Main10 4:4:4
|
||||
// Bit 11: ???
|
||||
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
|
||||
String str = getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||
if (str != null) {
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getGpuType(String serverInfo) throws XmlPullParserException, IOException {
|
||||
return getXmlString(serverInfo, "gputype");
|
||||
}
|
||||
|
||||
public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||
return getXmlString(serverInfo, "GfeVersion");
|
||||
}
|
||||
|
||||
public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException {
|
||||
// Only allow 4K on GFE 3.x
|
||||
String gfeVersionStr = getXmlString(serverInfo, "GfeVersion");
|
||||
if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getCurrentGame(String serverInfo) throws IOException, XmlPullParserException {
|
||||
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
||||
// has the semantics that its name would indicate. To contain the effects of this change as much
|
||||
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||
String serverState = getXmlString(serverInfo, "state");
|
||||
if (serverState != null && serverState.endsWith("_SERVER_BUSY")) {
|
||||
String game = getXmlString(serverInfo, "currentgame");
|
||||
return Integer.parseInt(game);
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public NvApp getAppById(int appId) throws IOException, XmlPullParserException {
|
||||
LinkedList<NvApp> appList = getAppList();
|
||||
for (NvApp appFromList : appList) {
|
||||
if (appFromList.getAppId() == appId) {
|
||||
return appFromList;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* NOTE: Only use this function if you know what you're doing.
|
||||
* It's totally valid to have two apps named the same thing,
|
||||
* or even nothing at all! Look apps up by ID if at all possible
|
||||
* using the above function */
|
||||
public NvApp getAppByName(String appName) throws IOException, XmlPullParserException {
|
||||
LinkedList<NvApp> appList = getAppList();
|
||||
for (NvApp appFromList : appList) {
|
||||
if (appFromList.getAppName().equalsIgnoreCase(appName)) {
|
||||
return appFromList;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public PairingManager getPairingManager() {
|
||||
return pm;
|
||||
}
|
||||
|
||||
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
|
||||
xpp.setInput(r);
|
||||
int eventType = xpp.getEventType();
|
||||
LinkedList<NvApp> appList = new LinkedList<NvApp>();
|
||||
Stack<String> currentTag = new Stack<String>();
|
||||
boolean rootTerminated = false;
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case (XmlPullParser.START_TAG):
|
||||
if (xpp.getName().equals("root")) {
|
||||
verifyResponseStatus(xpp);
|
||||
}
|
||||
currentTag.push(xpp.getName());
|
||||
if (xpp.getName().equals("App")) {
|
||||
appList.addLast(new NvApp());
|
||||
}
|
||||
break;
|
||||
case (XmlPullParser.END_TAG):
|
||||
currentTag.pop();
|
||||
if (xpp.getName().equals("root")) {
|
||||
rootTerminated = true;
|
||||
}
|
||||
break;
|
||||
case (XmlPullParser.TEXT):
|
||||
NvApp app = appList.getLast();
|
||||
if (currentTag.peek().equals("AppTitle")) {
|
||||
app.setAppName(xpp.getText().trim());
|
||||
} else if (currentTag.peek().equals("ID")) {
|
||||
app.setAppId(xpp.getText().trim());
|
||||
} else if (currentTag.peek().equals("IsHdrSupported")) {
|
||||
app.setHdrSupported(xpp.getText().trim().equals("1"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
|
||||
// Throw a malformed XML exception if we've not seen the root tag ended
|
||||
if (!rootTerminated) {
|
||||
throw new XmlPullParserException("Malformed XML: Root tag was not terminated");
|
||||
}
|
||||
|
||||
// Ensure that all apps in the list are initialized
|
||||
ListIterator<NvApp> i = appList.listIterator();
|
||||
while (i.hasNext()) {
|
||||
NvApp app = i.next();
|
||||
|
||||
// Remove uninitialized apps
|
||||
if (!app.isInitialized()) {
|
||||
LimeLog.warning("GFE returned incomplete app: "+app.getAppId()+" "+app.getAppName());
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return appList;
|
||||
}
|
||||
|
||||
public String getAppListRaw() throws MalformedURLException, IOException {
|
||||
return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true);
|
||||
}
|
||||
|
||||
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
|
||||
if (verbose) {
|
||||
// Use the raw function so the app list is printed
|
||||
return getAppListByReader(new StringReader(getAppListRaw()));
|
||||
}
|
||||
else {
|
||||
ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true);
|
||||
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
|
||||
resp.close();
|
||||
return appList;
|
||||
}
|
||||
}
|
||||
|
||||
public void unpair() throws IOException {
|
||||
openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true);
|
||||
}
|
||||
|
||||
public InputStream getBoxArt(NvApp app) throws IOException {
|
||||
ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() +
|
||||
"&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
||||
return resp.byteStream();
|
||||
}
|
||||
|
||||
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||
int[] appVersionQuad = getServerAppVersionQuad(serverInfo);
|
||||
if (appVersionQuad != null) {
|
||||
return appVersionQuad[0];
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
|
||||
try {
|
||||
String serverVersion = getServerVersion(serverInfo);
|
||||
if (serverVersion == null) {
|
||||
LimeLog.warning("Missing server version field");
|
||||
return null;
|
||||
}
|
||||
String[] serverVersionSplit = serverVersion.split("\\.");
|
||||
if (serverVersionSplit.length != 4) {
|
||||
LimeLog.warning("Malformed server version field");
|
||||
return null;
|
||||
}
|
||||
int[] ret = new int[serverVersionSplit.length];
|
||||
for (int i = 0; i < ret.length; i++) {
|
||||
ret[i] = Integer.parseInt(serverVersionSplit[i]);
|
||||
}
|
||||
return ret;
|
||||
} catch (NumberFormatException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 0 : context.streamConfig.getLaunchRefreshRate();
|
||||
|
||||
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
|
||||
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
|
||||
// 360p or 480p, but it is not ideal for 1440p and other resolutions.
|
||||
// When we detect an unsupported resolution, disable SOPS unless it's under 720p.
|
||||
// FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list
|
||||
boolean enableSops = context.streamConfig.getSops();
|
||||
if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 &&
|
||||
context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 &&
|
||||
context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) {
|
||||
LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight);
|
||||
enableSops = false;
|
||||
}
|
||||
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps +
|
||||
"/launch?" + buildUniqueIdUuidString() +
|
||||
"&appid=" + appId +
|
||||
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps +
|
||||
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
|
||||
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||
"&rikeyid="+context.riKeyId +
|
||||
(!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") +
|
||||
"&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) +
|
||||
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo() +
|
||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
|
||||
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
|
||||
false);
|
||||
String gameSession = getXmlString(xmlStr, "gamesession");
|
||||
return gameSession != null && !gameSession.equals("0");
|
||||
}
|
||||
|
||||
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() +
|
||||
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||
"&rikeyid="+context.riKeyId +
|
||||
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
|
||||
false);
|
||||
String resume = getXmlString(xmlStr, "resume");
|
||||
return Integer.parseInt(resume) != 0;
|
||||
}
|
||||
|
||||
public boolean quitApp() throws IOException, XmlPullParserException {
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false);
|
||||
String cancel = getXmlString(xmlStr, "cancel");
|
||||
if (Integer.parseInt(cancel) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
if (getCurrentGame(getServerInfo()) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw new GfeHttpResponseException(599, "");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.security.cert.Certificate;
|
||||
import java.io.*;
|
||||
import java.security.*;
|
||||
import java.security.cert.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
public class PairingManager {
|
||||
|
||||
private NvHTTP http;
|
||||
|
||||
private PrivateKey pk;
|
||||
private X509Certificate cert;
|
||||
private SecretKey aesKey;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
private X509Certificate serverCert;
|
||||
|
||||
public enum PairState {
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
}
|
||||
|
||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||
this.http = http;
|
||||
this.cert = cryptoProvider.getClientCertificate();
|
||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||
this.pk = cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||
{
|
||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||
if (certText != null) {
|
||||
byte[] certBytes = hexToBytes(certText);
|
||||
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateRandomBytes(int length)
|
||||
{
|
||||
byte[] rand = new byte[length];
|
||||
new SecureRandom().nextBytes(rand);
|
||||
return rand;
|
||||
}
|
||||
|
||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||
return saltedPin;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
byte[] signature = new byte[256];
|
||||
sig.sign(signature, 0, signature.length);
|
||||
return signature;
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||
blockRoundedSize, fullDecrypted);
|
||||
return fullDecrypted;
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static String generatePinString() {
|
||||
Random r = new Random();
|
||||
return String.format((Locale)null, "%d%d%d%d",
|
||||
r.nextInt(10), r.nextInt(10),
|
||||
r.nextInt(10), r.nextInt(10));
|
||||
}
|
||||
|
||||
public X509Certificate getPairedCert() {
|
||||
return serverCert;
|
||||
}
|
||||
|
||||
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||
PairingHashAlgorithm hashAlgo;
|
||||
|
||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||
if (serverMajorVersion >= 7) {
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = new Sha256PairingHash();
|
||||
}
|
||||
else {
|
||||
// Prior to Gen 7, SHA-1 is used
|
||||
hashAlgo = new Sha1PairingHash();
|
||||
}
|
||||
|
||||
// Generate a salt for hashing the PIN
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
byte[] saltAndPin = saltPin(salt, pin);
|
||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||
|
||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||
// because the user must enter the PIN before the server responds
|
||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||
false);
|
||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Save this cert for retrieval later
|
||||
serverCert = extractPlainCert(getCert);
|
||||
if (serverCert == null) {
|
||||
// Attempting to pair while another device is pairing will cause GFE
|
||||
// to give an empty cert in the response.
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
// Require this cert for TLS to this host
|
||||
http.setServerCert(serverCert);
|
||||
|
||||
// Generate a random challenge and encrypt it with our AES key
|
||||
byte[] randomChallenge = generateRandomBytes(16);
|
||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||
|
||||
// Send the encrypted challenge to the server
|
||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Decode the server's response and subsequent challenge
|
||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||
|
||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||
byte[] clientSecret = generateRandomBytes(16);
|
||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Get the server's signed secret
|
||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||
|
||||
// Ensure the authenticity of the data
|
||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Looks like a MITM
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Probably got the wrong PIN
|
||||
return PairState.PIN_WRONG;
|
||||
}
|
||||
|
||||
// Send the server our signed secret
|
||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
private interface PairingHashAlgorithm {
|
||||
int getHashLength();
|
||||
byte[] hashData(byte[] data);
|
||||
}
|
||||
|
||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class ControllerPacket {
|
||||
public static final short A_FLAG = 0x1000;
|
||||
public static final short B_FLAG = 0x2000;
|
||||
public static final short X_FLAG = 0x4000;
|
||||
public static final short Y_FLAG = (short)0x8000;
|
||||
public static final short UP_FLAG = 0x0001;
|
||||
public static final short DOWN_FLAG = 0x0002;
|
||||
public static final short LEFT_FLAG = 0x0004;
|
||||
public static final short RIGHT_FLAG = 0x0008;
|
||||
public static final short LB_FLAG = 0x0100;
|
||||
public static final short RB_FLAG = 0x0200;
|
||||
public static final short PLAY_FLAG = 0x0010;
|
||||
public static final short BACK_FLAG = 0x0020;
|
||||
public static final short LS_CLK_FLAG = 0x0040;
|
||||
public static final short RS_CLK_FLAG = 0x0080;
|
||||
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class KeyboardPacket {
|
||||
public static final byte KEY_DOWN = 0x03;
|
||||
public static final byte KEY_UP = 0x04;
|
||||
|
||||
public static final byte MODIFIER_SHIFT = 0x01;
|
||||
public static final byte MODIFIER_CTRL = 0x02;
|
||||
public static final byte MODIFIER_ALT = 0x04;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class MouseButtonPacket {
|
||||
public static final byte PRESS_EVENT = 0x07;
|
||||
public static final byte RELEASE_EVENT = 0x08;
|
||||
|
||||
public static final byte BUTTON_LEFT = 0x01;
|
||||
public static final byte BUTTON_MIDDLE = 0x02;
|
||||
public static final byte BUTTON_RIGHT = 0x03;
|
||||
public static final byte BUTTON_X1 = 0x04;
|
||||
public static final byte BUTTON_X2 = 0x05;
|
||||
}
|
||||
275
app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java
Normal file
275
app/src/main/java/com/limelight/nvstream/jni/MoonBridge.java
Normal file
@@ -0,0 +1,275 @@
|
||||
package com.limelight.nvstream.jni;
|
||||
|
||||
import com.limelight.nvstream.NvConnectionListener;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
public class MoonBridge {
|
||||
/* See documentation in Limelight.h for information about these functions and constants */
|
||||
|
||||
public static final AudioConfiguration AUDIO_CONFIGURATION_STEREO = new AudioConfiguration(2, 0x3);
|
||||
public static final AudioConfiguration AUDIO_CONFIGURATION_51_SURROUND = new AudioConfiguration(6, 0x3F);
|
||||
public static final AudioConfiguration AUDIO_CONFIGURATION_71_SURROUND = new AudioConfiguration(8, 0x63F);
|
||||
|
||||
public static final int VIDEO_FORMAT_H264 = 0x0001;
|
||||
public static final int VIDEO_FORMAT_H265 = 0x0100;
|
||||
public static final int VIDEO_FORMAT_H265_MAIN10 = 0x0200;
|
||||
|
||||
public static final int VIDEO_FORMAT_MASK_H264 = 0x00FF;
|
||||
public static final int VIDEO_FORMAT_MASK_H265 = 0xFF00;
|
||||
|
||||
public static final int BUFFER_TYPE_PICDATA = 0;
|
||||
public static final int BUFFER_TYPE_SPS = 1;
|
||||
public static final int BUFFER_TYPE_PPS = 2;
|
||||
public static final int BUFFER_TYPE_VPS = 3;
|
||||
|
||||
public static final int CAPABILITY_DIRECT_SUBMIT = 1;
|
||||
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC = 2;
|
||||
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC = 4;
|
||||
|
||||
public static final int DR_OK = 0;
|
||||
public static final int DR_NEED_IDR = -1;
|
||||
|
||||
public static final int CONN_STATUS_OKAY = 0;
|
||||
public static final int CONN_STATUS_POOR = 1;
|
||||
|
||||
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
|
||||
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
|
||||
|
||||
private static AudioRenderer audioRenderer;
|
||||
private static VideoDecoderRenderer videoRenderer;
|
||||
private static NvConnectionListener connectionListener;
|
||||
|
||||
static {
|
||||
System.loadLibrary("moonlight-core");
|
||||
init();
|
||||
}
|
||||
|
||||
public static int CAPABILITY_SLICES_PER_FRAME(byte slices) {
|
||||
return slices << 24;
|
||||
}
|
||||
|
||||
public static class AudioConfiguration {
|
||||
public final int channelCount;
|
||||
public final int channelMask;
|
||||
|
||||
public AudioConfiguration(int channelCount, int channelMask) {
|
||||
this.channelCount = channelCount;
|
||||
this.channelMask = channelMask;
|
||||
}
|
||||
|
||||
// Creates an AudioConfiguration from the integer value returned by moonlight-common-c
|
||||
// See CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION() and CHANNEL_MASK_FROM_AUDIO_CONFIGURATION()
|
||||
// in Limelight.h
|
||||
private AudioConfiguration(int audioConfiguration) {
|
||||
// Check the magic byte before decoding to make sure we got something that's actually
|
||||
// a MAKE_AUDIO_CONFIGURATION()-based value and not something else like an older version
|
||||
// hardcoded AUDIO_CONFIGURATION value from an earlier version of moonlight-common-c.
|
||||
if ((audioConfiguration & 0xFF) != 0xCA) {
|
||||
throw new IllegalArgumentException("Audio configuration has invalid magic byte!");
|
||||
}
|
||||
|
||||
this.channelCount = (audioConfiguration >> 8) & 0xFF;
|
||||
this.channelMask = (audioConfiguration >> 16) & 0xFFFF;
|
||||
}
|
||||
|
||||
// See SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION() in Limelight.h
|
||||
public int getSurroundAudioInfo() {
|
||||
return channelMask << 16 | channelCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof AudioConfiguration) {
|
||||
AudioConfiguration that = (AudioConfiguration)obj;
|
||||
return this.toInt() == that.toInt();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toInt();
|
||||
}
|
||||
|
||||
// Returns the integer value expected by moonlight-common-c
|
||||
// See MAKE_AUDIO_CONFIGURATION() in Limelight.h
|
||||
public int toInt() {
|
||||
return ((channelMask) << 16) | (channelCount << 8) | 0xCA;
|
||||
}
|
||||
}
|
||||
|
||||
public static int bridgeDrSetup(int videoFormat, int width, int height, int redrawRate) {
|
||||
if (videoRenderer != null) {
|
||||
return videoRenderer.setup(videoFormat, width, height, redrawRate);
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeDrStart() {
|
||||
if (videoRenderer != null) {
|
||||
videoRenderer.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeDrStop() {
|
||||
if (videoRenderer != null) {
|
||||
videoRenderer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeDrCleanup() {
|
||||
if (videoRenderer != null) {
|
||||
videoRenderer.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public static int bridgeDrSubmitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength,
|
||||
int decodeUnitType,
|
||||
int frameNumber, long receiveTimeMs) {
|
||||
if (videoRenderer != null) {
|
||||
return videoRenderer.submitDecodeUnit(decodeUnitData, decodeUnitLength,
|
||||
decodeUnitType, frameNumber, receiveTimeMs);
|
||||
}
|
||||
else {
|
||||
return DR_OK;
|
||||
}
|
||||
}
|
||||
|
||||
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||
if (audioRenderer != null) {
|
||||
return audioRenderer.setup(new AudioConfiguration(audioConfiguration), sampleRate, samplesPerFrame);
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeArStart() {
|
||||
if (audioRenderer != null) {
|
||||
audioRenderer.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeArStop() {
|
||||
if (audioRenderer != null) {
|
||||
audioRenderer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeArCleanup() {
|
||||
if (audioRenderer != null) {
|
||||
audioRenderer.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeArPlaySample(short[] pcmData) {
|
||||
if (audioRenderer != null) {
|
||||
audioRenderer.playDecodedAudio(pcmData);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClStageStarting(int stage) {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.stageStarting(getStageName(stage));
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClStageComplete(int stage) {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.stageComplete(getStageName(stage));
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClStageFailed(int stage, int errorCode) {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.stageFailed(getStageName(stage), errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClConnectionStarted() {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.connectionStarted();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClConnectionTerminated(int errorCode) {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.connectionTerminated(errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.rumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bridgeClConnectionStatusUpdate(int connectionStatus) {
|
||||
if (connectionListener != null) {
|
||||
connectionListener.connectionStatusUpdate(connectionStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) {
|
||||
MoonBridge.videoRenderer = videoRenderer;
|
||||
MoonBridge.audioRenderer = audioRenderer;
|
||||
MoonBridge.connectionListener = connectionListener;
|
||||
}
|
||||
|
||||
public static void cleanupBridge() {
|
||||
MoonBridge.videoRenderer = null;
|
||||
MoonBridge.audioRenderer = null;
|
||||
MoonBridge.connectionListener = null;
|
||||
}
|
||||
|
||||
public static native int startConnection(String address, String appVersion, String gfeVersion,
|
||||
int width, int height, int fps,
|
||||
int bitrate, int packetSize, int streamingRemotely,
|
||||
int audioConfiguration, boolean supportsHevc,
|
||||
boolean enableHdr,
|
||||
int hevcBitratePercentageMultiplier,
|
||||
int clientRefreshRateX100,
|
||||
byte[] riAesKey, byte[] riAesIv,
|
||||
int videoCapabilities);
|
||||
|
||||
public static native void stopConnection();
|
||||
|
||||
public static native void interruptConnection();
|
||||
|
||||
public static native void sendMouseMove(short deltaX, short deltaY);
|
||||
|
||||
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
|
||||
|
||||
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
|
||||
|
||||
public static native void sendMultiControllerInput(short controllerNumber,
|
||||
short activeGamepadMask, short buttonFlags,
|
||||
byte leftTrigger, byte rightTrigger,
|
||||
short leftStickX, short leftStickY,
|
||||
short rightStickX, short rightStickY);
|
||||
|
||||
public static native void sendControllerInput(short buttonFlags,
|
||||
byte leftTrigger, byte rightTrigger,
|
||||
short leftStickX, short leftStickY,
|
||||
short rightStickX, short rightStickY);
|
||||
|
||||
public static native void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier);
|
||||
|
||||
public static native void sendMouseScroll(byte scrollClicks);
|
||||
|
||||
public static native void sendMouseHighResScroll(short scrollAmount);
|
||||
|
||||
public static native String getStageName(int stage);
|
||||
|
||||
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
||||
|
||||
public static native int getPendingAudioDuration();
|
||||
|
||||
public static native int getPendingVideoFrames();
|
||||
|
||||
public static native void init();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.limelight.nvstream.mdns;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class MdnsComputer {
|
||||
private InetAddress localAddr;
|
||||
private Inet6Address v6Addr;
|
||||
private String name;
|
||||
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||
this.name = name;
|
||||
this.localAddr = localAddress;
|
||||
this.v6Addr = v6Addr;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public InetAddress getLocalAddress() {
|
||||
return localAddr;
|
||||
}
|
||||
|
||||
public Inet6Address getIpv6Address() {
|
||||
return v6Addr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MdnsComputer) {
|
||||
MdnsComputer other = (MdnsComputer)o;
|
||||
|
||||
if (!other.name.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((other.localAddr != null && localAddr == null) ||
|
||||
(other.localAddr == null && localAddr != null) ||
|
||||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((other.v6Addr != null && v6Addr == null) ||
|
||||
(other.v6Addr == null && v6Addr != null) ||
|
||||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package com.limelight.nvstream.mdns;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import javax.jmdns.JmmDNS;
|
||||
import javax.jmdns.NetworkTopologyDiscovery;
|
||||
import javax.jmdns.ServiceEvent;
|
||||
import javax.jmdns.ServiceInfo;
|
||||
import javax.jmdns.ServiceListener;
|
||||
import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class MdnsDiscoveryAgent implements ServiceListener {
|
||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
||||
|
||||
private MdnsDiscoveryListener listener;
|
||||
private Thread discoveryThread;
|
||||
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
||||
private HashSet<String> pendingResolution = new HashSet<String>();
|
||||
|
||||
// The resolver factory's instance member has a static lifetime which
|
||||
// means our ref count and listener must be static also.
|
||||
private static int resolverRefCount = 0;
|
||||
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceAdded(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceRemoved(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceResolved(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
||||
@Override
|
||||
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
||||
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
||||
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
||||
try {
|
||||
if (!networkInterface.isUp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
if (!networkInterface.supportsMulticast()) {
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
|
||||
if (networkInterface.isLoopback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static {
|
||||
// Override jmDNS's default topology discovery class with ours
|
||||
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
||||
@Override
|
||||
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
||||
return new MyNetworkTopologyDiscovery();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static JmmDNS referenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
JmmDNS instance = JmmDNS.Factory.getInstance();
|
||||
if (++resolverRefCount == 1) {
|
||||
// This will cause the listener to be invoked for known hosts immediately.
|
||||
// JmDNS only supports one listener per service, so we have to do this here
|
||||
// with a static listener.
|
||||
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static void dereferenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
if (--resolverRefCount == 0) {
|
||||
try {
|
||||
JmmDNS.Factory.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.remove(info.getName());
|
||||
}
|
||||
|
||||
try {
|
||||
handleServiceInfo(info);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Invalid DNS response
|
||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||
return addr;
|
||||
}
|
||||
// fc00::/7 - ULAs
|
||||
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress()) {
|
||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
||||
// First try to find a link local address, so we can match the interface identifier
|
||||
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||
|
||||
// We will try once to match a SLAAC interface suffix, then
|
||||
// pick the first matching address
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
// We assume the addresses are already sorted in descending order
|
||||
// of preference from Bonjour.
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
||||
// Link-local, site-local, and loopback aren't global
|
||||
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] addrBytes = addr.getAddress();
|
||||
|
||||
// 2002::/16
|
||||
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
||||
// 6to4 has horrible performance
|
||||
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2001::/32
|
||||
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
||||
// Teredo also has horrible performance
|
||||
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// fc00::/7
|
||||
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
||||
// ULAs aren't global
|
||||
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare the final 64-bit interface identifier and skip the address
|
||||
// if it doesn't match our link-local address.
|
||||
if (linkLocalAddr != null && tries == 0) {
|
||||
boolean matched = true;
|
||||
|
||||
for (int i = 8; i < 16; i++) {
|
||||
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
||||
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
||||
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
||||
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
||||
|
||||
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
||||
|
||||
// Add a computer object for each IPv4 address reported by the PC
|
||||
for (Inet4Address v4Addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there were no IPv4 addresses, use IPv6 for registration
|
||||
if (v4Addrs.length == 0) {
|
||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||
|
||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||
if (computers.put(v6LocalAddr != null ?
|
||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startDiscovery(final int discoveryIntervalMs) {
|
||||
// Kill any existing discovery before starting a new one
|
||||
stopDiscovery();
|
||||
|
||||
// Add our listener to the set
|
||||
synchronized (listeners) {
|
||||
listeners.add(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
discoveryThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// This may result in listener callbacks so we must register
|
||||
// our listener first.
|
||||
JmmDNS resolver = referenceResolver();
|
||||
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
// Start an mDNS request
|
||||
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
||||
|
||||
// Run service resolution again for pending machines
|
||||
ArrayList<String> pendingNames;
|
||||
synchronized (pendingResolution) {
|
||||
pendingNames = new ArrayList<String>(pendingResolution);
|
||||
}
|
||||
for (String name : pendingNames) {
|
||||
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
||||
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
||||
if (infos != null && infos.length != 0) {
|
||||
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
||||
for (ServiceInfo svcinfo : infos) {
|
||||
handleResolvedServiceInfo(svcinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the next polling interval
|
||||
try {
|
||||
Thread.sleep(discoveryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Dereference the resolver
|
||||
dereferenceResolver();
|
||||
}
|
||||
}
|
||||
};
|
||||
discoveryThread.setName("mDNS Discovery Thread");
|
||||
discoveryThread.start();
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
// Remove our listener from the set
|
||||
synchronized (listeners) {
|
||||
listeners.remove(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
// If there's already a running thread, interrupt it
|
||||
if (discoveryThread != null) {
|
||||
discoveryThread.interrupt();
|
||||
discoveryThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
synchronized (computers) {
|
||||
return new ArrayList<MdnsComputer>(computers.values());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
||||
|
||||
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
||||
if (info == null) {
|
||||
// This machine is pending resolution
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.add(event.getInfo().getName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("mDNS: Resolved (blocking)");
|
||||
handleResolvedServiceInfo(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
||||
|
||||
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
||||
for (Inet4Address addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
||||
for (Inet6Address addr : v6Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
// We handle this synchronously
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.limelight.nvstream.mdns;
|
||||
|
||||
public interface MdnsDiscoveryListener {
|
||||
void notifyComputerAdded(MdnsComputer computer);
|
||||
void notifyComputerRemoved(MdnsComputer computer);
|
||||
void notifyDiscoveryFailure(Exception e);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.limelight.nvstream.wol;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Scanner;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public class WakeOnLanSender {
|
||||
private static final int[] PORTS_TO_TRY = new int[] {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
||||
};
|
||||
|
||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||
DatagramSocket sock = new DatagramSocket(0);
|
||||
byte[] payload = createWolPayload(computer);
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
|
||||
try {
|
||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||
// The broadcast address is required to avoid stale ARP cache entries
|
||||
// making the sleeping machine unreachable.
|
||||
for (String unresolvedAddress : new String[] {
|
||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||
}) {
|
||||
if (unresolvedAddress == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||
// Try all the ports for each resolved address
|
||||
for (int port : PORTS_TO_TRY) {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(resolvedAddress);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// We may have addresses that don't resolve on this subnet,
|
||||
// but don't throw and exit the whole function if that happens.
|
||||
// We'll throw it at the end if we didn't send a single packet.
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
|
||||
// Propagate the DNS resolution exception if we didn't
|
||||
// manage to get a single packet out to the host.
|
||||
if (!sentWolPacket && lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] macStringToBytes(String macAddress) {
|
||||
byte[] macBytes = new byte[6];
|
||||
@SuppressWarnings("resource")
|
||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
scan.close();
|
||||
return macBytes;
|
||||
}
|
||||
|
||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||
byte[] payload = new byte[102];
|
||||
byte[] macAddress = macStringToBytes(computer.macAddress);
|
||||
int i;
|
||||
|
||||
// 6 bytes of FF
|
||||
for (i = 0; i < 6; i++) {
|
||||
payload[i] = (byte)0xFF;
|
||||
}
|
||||
|
||||
// 16 repetitions of the MAC address
|
||||
for (int j = 0; j < 16; j++) {
|
||||
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
||||
i += macAddress.length;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
@@ -9,8 +10,11 @@ import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
@@ -91,14 +95,28 @@ public class AddComputerManually extends Activity {
|
||||
}
|
||||
|
||||
private void doAddPc(String host) {
|
||||
String msg;
|
||||
boolean wrongSiteLocal = false;
|
||||
boolean success;
|
||||
|
||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||
getResources().getString(R.string.msg_add_pc), false);
|
||||
|
||||
success = managerBinder.addComputerBlocking(host, true);
|
||||
try {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
details.manualAddress = host;
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
|
||||
details.serverCert = http.getCertificateIfTrusted();
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
success = managerBinder.addComputerBlocking(details);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||
e.printStackTrace();
|
||||
success = false;
|
||||
}
|
||||
if (!success){
|
||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.limelight.preferences;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
@@ -6,8 +6,14 @@ import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
private static final String LEGACY_ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
||||
|
||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
||||
static final String FPS_PREF_STRING = "list_fps";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||
@@ -15,11 +21,11 @@ public class PreferenceConfiguration {
|
||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
||||
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
|
||||
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
|
||||
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";
|
||||
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
|
||||
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";
|
||||
@@ -27,87 +33,163 @@ public class PreferenceConfiguration {
|
||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||
private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
|
||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
||||
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
||||
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
|
||||
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
|
||||
private static final String LATENCY_TOAST_PREF_STRING = "checkbox_enable_post_stream_toast";
|
||||
|
||||
private static final int BITRATE_DEFAULT_360_30 = 1000;
|
||||
private static final int BITRATE_DEFAULT_360_60 = 2000;
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5000;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10000;
|
||||
private static final int BITRATE_DEFAULT_1080_30 = 10000;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 20000;
|
||||
private static final int BITRATE_DEFAULT_4K_30 = 40000;
|
||||
private static final int BITRATE_DEFAULT_4K_60 = 80000;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
static final String DEFAULT_RESOLUTION = "1280x720";
|
||||
static final String DEFAULT_FPS = "60";
|
||||
private static final boolean DEFAULT_STRETCH = false;
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
private static final boolean DEFAULT_HOST_AUDIO = false;
|
||||
private static final int DEFAULT_DEADZONE = 15;
|
||||
private static final int DEFAULT_OPACITY = 90;
|
||||
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;
|
||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
||||
private static final boolean DEFAULT_BATTERY_SAVER = false;
|
||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||
private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false;
|
||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
|
||||
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
|
||||
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
|
||||
private static final boolean DEFAULT_LATENCY_TOAST = false;
|
||||
|
||||
public static final int FORCE_H265_ON = -1;
|
||||
public static final int AUTOSELECT_H265 = 0;
|
||||
public static final int FORCE_H265_OFF = 1;
|
||||
|
||||
public static final String RES_360P = "640x360";
|
||||
public static final String RES_480P = "854x480";
|
||||
public static final String RES_720P = "1280x720";
|
||||
public static final String RES_1080P = "1920x1080";
|
||||
public static final String RES_1440P = "2560x1440";
|
||||
public static final String RES_4K = "3840x2160";
|
||||
|
||||
public int width, height, fps;
|
||||
public int bitrate;
|
||||
public int videoFormat;
|
||||
public int deadzonePercentage;
|
||||
public int oscOpacity;
|
||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||
public String language;
|
||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||
public boolean smallIconMode, multiController, usbDriver, flipFaceButtons;
|
||||
public boolean onscreenController;
|
||||
public boolean onlyL3R3;
|
||||
public boolean disableFrameDrop;
|
||||
public boolean enableHdr;
|
||||
public boolean enablePip;
|
||||
public boolean enablePerfOverlay;
|
||||
public boolean enableLatencyToast;
|
||||
public boolean bindAllUsb;
|
||||
public boolean mouseEmulation;
|
||||
public boolean mouseNavButtons;
|
||||
public boolean unlockFps;
|
||||
public boolean vibrateOsc;
|
||||
public boolean vibrateFallbackToDevice;
|
||||
public boolean touchscreenTrackpad;
|
||||
public MoonBridge.AudioConfiguration audioConfiguration;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("360p30")) {
|
||||
return BITRATE_DEFAULT_360_30;
|
||||
private static String convertFromLegacyResolutionString(String resString) {
|
||||
if (resString.equalsIgnoreCase("360p")) {
|
||||
return RES_360P;
|
||||
}
|
||||
else if (resFpsString.equals("360p60")) {
|
||||
return BITRATE_DEFAULT_360_60;
|
||||
else if (resString.equalsIgnoreCase("480p")) {
|
||||
return RES_480P;
|
||||
}
|
||||
else if (resFpsString.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
else if (resString.equalsIgnoreCase("720p")) {
|
||||
return RES_720P;
|
||||
}
|
||||
else if (resFpsString.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
else if (resString.equalsIgnoreCase("1080p")) {
|
||||
return RES_1080P;
|
||||
}
|
||||
else if (resFpsString.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
else if (resString.equalsIgnoreCase("1440p")) {
|
||||
return RES_1440P;
|
||||
}
|
||||
else if (resFpsString.equals("1080p60")) {
|
||||
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 if (resString.equalsIgnoreCase("4K")) {
|
||||
return RES_4K;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
// Should be unreachable
|
||||
return RES_720P;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getWidthFromResolutionString(String resString) {
|
||||
return Integer.parseInt(resString.split("x")[0]);
|
||||
}
|
||||
|
||||
private static int getHeightFromResolutionString(String resString) {
|
||||
return Integer.parseInt(resString.split("x")[1]);
|
||||
}
|
||||
|
||||
private static String getResolutionString(int width, int height) {
|
||||
switch (height) {
|
||||
case 360:
|
||||
return RES_360P;
|
||||
case 480:
|
||||
return RES_480P;
|
||||
default:
|
||||
case 720:
|
||||
return RES_720P;
|
||||
case 1080:
|
||||
return RES_1080P;
|
||||
case 1440:
|
||||
return RES_1440P;
|
||||
case 2160:
|
||||
return RES_4K;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(String resString, String fpsString) {
|
||||
int width = getWidthFromResolutionString(resString);
|
||||
int height = getHeightFromResolutionString(resString);
|
||||
int fps = Integer.parseInt(fpsString);
|
||||
|
||||
// This table prefers 16:10 resolutions because they are
|
||||
// only slightly more pixels than the 16:9 equivalents, so
|
||||
// we don't want to bump those 16:10 resolutions up to the
|
||||
// next 16:9 slot.
|
||||
//
|
||||
// This logic is shamelessly stolen from Moonlight Qt:
|
||||
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
||||
|
||||
if (width * height <= 640 * 360) {
|
||||
return (int)(1000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 854 * 480) {
|
||||
return (int)(1500 * (fps / 30.0));
|
||||
}
|
||||
// This covers 1280x720 and 1280x800 too
|
||||
else if (width * height <= 1366 * 768) {
|
||||
return (int)(5000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 1920 * 1200) {
|
||||
return (int)(10000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 2560 * 1600) {
|
||||
return (int)(20000 * (fps / 30.0));
|
||||
}
|
||||
else /* if (width * height <= 3840 * 2160) */ {
|
||||
return (int)(40000 * (fps / 30.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +215,9 @@ public class PreferenceConfiguration {
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
||||
return getDefaultBitrate(
|
||||
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
||||
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||
}
|
||||
|
||||
private static int getVideoFormatValue(Context context) {
|
||||
@@ -161,9 +245,12 @@ public class PreferenceConfiguration {
|
||||
prefs.edit()
|
||||
.remove(BITRATE_PREF_STRING)
|
||||
.remove(BITRATE_PREF_OLD_STRING)
|
||||
.remove(RES_FPS_PREF_STRING)
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.remove(RESOLUTION_PREF_STRING)
|
||||
.remove(FPS_PREF_STRING)
|
||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||
.remove(ENABLE_HDR_PREF_STRING)
|
||||
.remove(UNLOCK_FPS_STRING)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -171,63 +258,115 @@ public class PreferenceConfiguration {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||
|
||||
// Migrate legacy preferences to the new locations
|
||||
if (prefs.contains(LEGACY_ENABLE_51_SURROUND_PREF_STRING)) {
|
||||
if (prefs.getBoolean(LEGACY_ENABLE_51_SURROUND_PREF_STRING, false)) {
|
||||
prefs.edit()
|
||||
.remove(LEGACY_ENABLE_51_SURROUND_PREF_STRING)
|
||||
.putString(AUDIO_CONFIG_PREF_STRING, "51")
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||
if (str != null) {
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else 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 {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
|
||||
.putString(FPS_PREF_STRING, ""+config.fps)
|
||||
.apply();
|
||||
}
|
||||
else {
|
||||
// Use the new preference location
|
||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
|
||||
// Convert legacy resolution strings to the new style
|
||||
if (!resStr.contains("x")) {
|
||||
resStr = PreferenceConfiguration.convertFromLegacyResolutionString(resStr);
|
||||
prefs.edit().putString(RESOLUTION_PREF_STRING, resStr).apply();
|
||||
}
|
||||
|
||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||
}
|
||||
|
||||
if (!prefs.contains(SMALL_ICONS_PREF_STRING)) {
|
||||
// We need to write small icon mode's default to disk for the settings page to display
|
||||
// the current state of the option properly
|
||||
prefs.edit().putBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)).apply();
|
||||
}
|
||||
|
||||
// This must happen after the preferences migration to ensure the preferences are populated
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||
if (config.bitrate == 0) {
|
||||
config.bitrate = getDefaultBitrate(context);
|
||||
}
|
||||
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
String audioConfig = prefs.getString(AUDIO_CONFIG_PREF_STRING, DEFAULT_AUDIO_CONFIG);
|
||||
if (audioConfig.equals("71")) {
|
||||
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_71_SURROUND;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
else if (audioConfig.equals("51")) {
|
||||
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else 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 {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
else /* if (audioConfig.equals("2")) */ {
|
||||
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
|
||||
config.videoFormat = getVideoFormatValue(context);
|
||||
|
||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||
|
||||
config.oscOpacity = prefs.getInt(OSC_OPACITY_PREF_STRING, DEFAULT_OPACITY);
|
||||
|
||||
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
|
||||
|
||||
// Checkbox preferences
|
||||
@@ -235,18 +374,24 @@ public class PreferenceConfiguration {
|
||||
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||
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);
|
||||
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||
config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
|
||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
|
||||
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
|
||||
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
@@ -78,6 +79,8 @@ public class SeekBarPreference extends DialogPreference
|
||||
valueText = new TextView(context);
|
||||
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
valueText.setTextSize(32);
|
||||
// Default text for value; hides bug where OnSeekBarChangeListener isn't called when opacity is 0%
|
||||
valueText.setText("0%");
|
||||
params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.os.Vibrator;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
@@ -14,6 +17,9 @@ import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.util.Range;
|
||||
import android.view.Display;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.PcView;
|
||||
@@ -24,6 +30,12 @@ import com.limelight.utils.UiHelper;
|
||||
public class StreamSettings extends Activity {
|
||||
private PreferenceConfiguration previousPrefs;
|
||||
|
||||
void reloadSettings() {
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -33,9 +45,7 @@ public class StreamSettings extends Activity {
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
setContentView(R.layout.activity_stream_settings);
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
reloadSettings();
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
}
|
||||
@@ -46,9 +56,7 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// 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)) {
|
||||
if (!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);
|
||||
@@ -58,12 +66,20 @@ public class StreamSettings extends Activity {
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
|
||||
private static void removeResolution(ListPreference pref, String prefix) {
|
||||
private void setValue(String preferenceKey, String value) {
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
pref.setValue(value);
|
||||
}
|
||||
|
||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||
int matchingCount = 0;
|
||||
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
// Count the number of matching entries we'll be removing
|
||||
for (CharSequence seq : pref.getEntryValues()) {
|
||||
if (seq.toString().startsWith(prefix)) {
|
||||
if (seq.toString().equalsIgnoreCase(value)) {
|
||||
matchingCount++;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +89,8 @@ public class StreamSettings extends Activity {
|
||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||
int outIndex = 0;
|
||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
|
||||
// Skip matching prefixes
|
||||
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||
// Skip matching values
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -83,11 +99,38 @@ public class StreamSettings extends Activity {
|
||||
outIndex++;
|
||||
}
|
||||
|
||||
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||
onMatched.run();
|
||||
}
|
||||
|
||||
// Update the preference with the new list
|
||||
pref.setEntries(entries);
|
||||
pref.setEntryValues(entryValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||
if (res == null) {
|
||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
}
|
||||
if (fps == null) {
|
||||
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = super.onCreateView(inflater, container, savedInstanceState);
|
||||
UiHelper.applyStatusBarPadding(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -98,18 +141,42 @@ public class StreamSettings extends Activity {
|
||||
// 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);
|
||||
{
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||
screen.removePreference(category);
|
||||
}
|
||||
|
||||
{
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_input_settings");
|
||||
category.removePreference(findPreference("checkbox_touchscreen_trackpad"));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PiP mode on devices pre-Oreo
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// Remove PiP mode on devices pre-Oreo or where the feature is not available (some low RAM devices)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
|
||||
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture")) {
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_basic_settings");
|
||||
(PreferenceCategory) findPreference("category_ui_settings");
|
||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||
}
|
||||
|
||||
// Remove the vibration options if the device can't vibrate
|
||||
if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) {
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_input_settings");
|
||||
category.removePreference(findPreference("checkbox_vibrate_fallback"));
|
||||
|
||||
// The entire OSC category may have already been removed by the touchscreen check above
|
||||
category = (PreferenceCategory) findPreference("category_onscreen_controls");
|
||||
if (category != null) {
|
||||
category.removePreference(findPreference("checkbox_vibrate_osc"));
|
||||
}
|
||||
}
|
||||
|
||||
int maxSupportedFps = 0;
|
||||
|
||||
// Hide non-supported resolution/FPS combinations
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||
@@ -134,9 +201,16 @@ public class StreamSettings extends Activity {
|
||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||
maxSupportedResW = 3840;
|
||||
}
|
||||
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
||||
maxSupportedResW = 2560;
|
||||
}
|
||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
||||
maxSupportedResW = 1920;
|
||||
}
|
||||
|
||||
if (candidate.getRefreshRate() > maxSupportedFps) {
|
||||
maxSupportedFps = (int)candidate.getRefreshRate();
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called to do runtime initialization before calling functions that evaluate
|
||||
@@ -186,20 +260,114 @@ public class StreamSettings extends Activity {
|
||||
|
||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
||||
|
||||
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
|
||||
if (maxSupportedResW != 0) {
|
||||
if (maxSupportedResW < 3840) {
|
||||
// 4K is unsupported
|
||||
removeResolution(resPref, "4K");
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P);
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 2560) {
|
||||
// 1440p is unsupported
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P);
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 1920) {
|
||||
// 1080p is unsupported
|
||||
removeResolution(resPref, "1080p");
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P);
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 720p
|
||||
}
|
||||
}
|
||||
|
||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
||||
// We give some extra room in case the FPS is rounded down
|
||||
if (maxSupportedFps < 118) {
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedFps < 88) {
|
||||
// 1080p is unsupported
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 30 FPS or 60 FPS
|
||||
}
|
||||
|
||||
// Android L introduces proper 7.1 surround sound support. Remove the 7.1 option
|
||||
// for earlier versions of Android to prevent AudioTrack initialization issues.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LimeLog.info("Excluding 7.1 surround sound option based on OS");
|
||||
removeValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "71", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setValue(PreferenceConfiguration.AUDIO_CONFIG_PREF_STRING, "51");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
||||
// that the unlock FPS option relies on to not massively increase latency.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
||||
}
|
||||
else {
|
||||
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// HACK: We need to let the preference change succeed before reinitializing to ensure
|
||||
// it's reflected in the new layout.
|
||||
final Handler h = new Handler();
|
||||
h.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Ensure the activity is still open when this timeout expires
|
||||
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
|
||||
if (settingsActivity != null) {
|
||||
settingsActivity.reloadSettings();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove HDR preference for devices below Nougat
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
LimeLog.info("Excluding HDR toggle based on OS");
|
||||
@@ -213,9 +381,12 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// We must now ensure our display is compatible with HDR10
|
||||
boolean foundHdr10 = false;
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
if (hdrCaps != null) {
|
||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,18 +400,27 @@ public class StreamSettings extends Activity {
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||
.apply();
|
||||
resetBitrateToDefault(prefs, valueStr, null);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
resetBitrateToDefault(prefs, null, valueStr);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
|
||||
@@ -62,9 +62,8 @@ public class StreamView extends SurfaceView {
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
// This callbacks allows us to override dumb IME behavior like when
|
||||
// Samsung's default keyboard consumes Shift+Space. We'll process
|
||||
// the input event directly if any modifier keys are down.
|
||||
if (inputCallbacks != null && event.getModifiers() != 0) {
|
||||
// Samsung's default keyboard consumes Shift+Space.
|
||||
if (inputCallbacks != null) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (inputCallbacks.handleKeyDown(event)) {
|
||||
return true;
|
||||
|
||||
@@ -3,41 +3,22 @@ package com.limelight.utils;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.limelight.HelpActivity;
|
||||
|
||||
public class HelpLauncher {
|
||||
|
||||
private static boolean isKnownBrowser(Context context, Intent i) {
|
||||
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
if (resolvedActivity == null) {
|
||||
// No browser
|
||||
return false;
|
||||
}
|
||||
|
||||
String name = resolvedActivity.activityInfo.name;
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
name = name.toLowerCase();
|
||||
return name.contains("chrome") || name.contains("firefox");
|
||||
}
|
||||
|
||||
private static void launchUrl(Context context, String url) {
|
||||
// Try to launch the default browser
|
||||
try {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
|
||||
// Several Android 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. We need to
|
||||
// be a bit more clever on these devices and detect if the browser is a legitimate
|
||||
// browser or just a fake error message activity.
|
||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
||||
isKnownBrowser(context, i)) {
|
||||
// Several Android 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. We used to try to be clever and check
|
||||
// the package name of the resolved intent, but it's not worth it anymore with Android 11's
|
||||
// package visibility changes. We'll just always use the WebView on Android TV.
|
||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||
context.startActivity(i);
|
||||
return;
|
||||
}
|
||||
|
||||
32
app/src/main/java/com/limelight/utils/NetHelper.java
Normal file
32
app/src/main/java/com/limelight/utils/NetHelper.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
|
||||
public class NetHelper {
|
||||
public static boolean isActiveNetworkVpn(Context context) {
|
||||
ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Network activeNetwork = connMgr.getActiveNetwork();
|
||||
if (activeNetwork != null) {
|
||||
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
|
||||
if (netCaps != null) {
|
||||
return netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||
if (activeNetworkInfo != null) {
|
||||
return activeNetworkInfo.getType() == ConnectivityManager.TYPE_VPN;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.Game;
|
||||
import com.limelight.R;
|
||||
import com.limelight.ShortcutTrampoline;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
@@ -13,13 +15,35 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
|
||||
public class ServerHelper {
|
||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||
computer.localAddress : computer.remoteAddress;
|
||||
return computer.activeAddress;
|
||||
}
|
||||
|
||||
public static Intent createPcShortcutIntent(Activity parent, ComputerDetails computer) {
|
||||
Intent i = new Intent(parent, ShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
return i;
|
||||
}
|
||||
|
||||
public static Intent createAppShortcutIntent(Activity parent, ComputerDetails computer, NvApp app) {
|
||||
Intent i = new Intent(parent, ShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
i.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||
i.putExtra(Game.EXTRA_APP_ID, ""+app.getAppId());
|
||||
i.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
return i;
|
||||
}
|
||||
|
||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||
@@ -30,20 +54,30 @@ public class ServerHelper {
|
||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||
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_UUID, computer.uuid);
|
||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||
try {
|
||||
if (computer.serverCert != null) {
|
||||
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||
}
|
||||
|
||||
public static void doQuit(final Activity parent,
|
||||
final String address,
|
||||
final ComputerDetails computer,
|
||||
final NvApp app,
|
||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||
final Runnable onComplete) {
|
||||
@@ -54,8 +88,8 @@ public class ServerHelper {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(address,
|
||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||
if (httpConn.quitApp()) {
|
||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||
} else {
|
||||
@@ -74,8 +108,9 @@ public class ServerHelper {
|
||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = parent.getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
message = e.getMessage();
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (onComplete != null) {
|
||||
onComplete.run();
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.AppViewShortcutTrampoline;
|
||||
import com.limelight.ShortcutTrampoline;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@@ -20,9 +22,10 @@ import java.util.List;
|
||||
public class ShortcutHelper {
|
||||
|
||||
private final ShortcutManager sm;
|
||||
private final Context context;
|
||||
private final Activity context;
|
||||
private final TvChannelHelper tvChannelHelper;
|
||||
|
||||
public ShortcutHelper(Context context) {
|
||||
public ShortcutHelper(Activity context) {
|
||||
this.context = context;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
sm = context.getSystemService(ShortcutManager.class);
|
||||
@@ -30,6 +33,7 @@ public class ShortcutHelper {
|
||||
else {
|
||||
sm = null;
|
||||
}
|
||||
this.tvChannelHelper = new TvChannelHelper(context);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
@@ -78,40 +82,39 @@ public class ShortcutHelper {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reportShortcutUsed(String id) {
|
||||
public void reportComputerShortcutUsed(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo sinfo = getInfoForId(id);
|
||||
if (sinfo != null) {
|
||||
sm.reportShortcutUsed(id);
|
||||
if (getInfoForId(computer.uuid) != null) {
|
||||
sm.reportShortcutUsed(computer.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
public void reportGameLaunched(ComputerDetails computer, NvApp app) {
|
||||
tvChannelHelper.createTvChannel(computer);
|
||||
tvChannelHelper.addGameToChannel(computer, app);
|
||||
}
|
||||
|
||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||
.setIntent(i)
|
||||
.setShortLabel(computerName)
|
||||
.setLongLabel(computerName)
|
||||
public void createAppViewShortcut(ComputerDetails computer, boolean forceAdd, boolean newlyPaired) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, computer.uuid)
|
||||
.setIntent(ServerHelper.createPcShortcutIntent(context, computer))
|
||||
.setShortLabel(computer.name)
|
||||
.setLongLabel(computer.name)
|
||||
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||
.build();
|
||||
|
||||
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||
ShortcutInfo existingSinfo = getInfoForId(computer.uuid);
|
||||
if (existingSinfo != null) {
|
||||
// Update in place
|
||||
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||
sm.enableShortcuts(Collections.singletonList(id));
|
||||
sm.enableShortcuts(Collections.singletonList(computer.uuid));
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
if (!isExistingDynamicShortcut(computer.uuid)) {
|
||||
// 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.
|
||||
@@ -121,18 +124,81 @@ public class ShortcutHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newlyPaired) {
|
||||
// Avoid hammering the channel API for each computer poll because it will throttle us
|
||||
tvChannelHelper.createTvChannel(computer);
|
||||
tvChannelHelper.requestChannelOnHomeScreen(computer);
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
|
||||
public void createAppViewShortcutForOnlineHost(ComputerDetails details) {
|
||||
createAppViewShortcut(details, false, false);
|
||||
}
|
||||
|
||||
public void disableShortcut(String id, CharSequence reason) {
|
||||
private String getShortcutIdForGame(ComputerDetails computer, NvApp app) {
|
||||
return computer.uuid + app.getAppId();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public boolean createPinnedGameShortcut(ComputerDetails computer, NvApp app, Bitmap iconBits) {
|
||||
if (sm.isRequestPinShortcutSupported()) {
|
||||
Icon appIcon;
|
||||
|
||||
if (iconBits != null) {
|
||||
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
|
||||
} else {
|
||||
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
|
||||
}
|
||||
|
||||
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, getShortcutIdForGame(computer, app))
|
||||
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
|
||||
.setShortLabel(app.getAppName() + " (" + computer.name + ")")
|
||||
.setIcon(appIcon)
|
||||
.build();
|
||||
|
||||
return sm.requestPinShortcut(sInfo, null);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void disableComputerShortcut(ComputerDetails computer, CharSequence reason) {
|
||||
tvChannelHelper.deleteChannel(computer);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo sinfo = getInfoForId(id);
|
||||
if (sinfo != null) {
|
||||
// Delete the computer shortcut itself
|
||||
if (getInfoForId(computer.uuid) != null) {
|
||||
sm.disableShortcuts(Collections.singletonList(computer.uuid), reason);
|
||||
}
|
||||
|
||||
// Delete all associated app shortcuts too
|
||||
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||
LinkedList<String> appShortcutIds = new LinkedList<>();
|
||||
for (ShortcutInfo info : shortcuts) {
|
||||
if (info.getId().startsWith(computer.uuid)) {
|
||||
appShortcutIds.add(info.getId());
|
||||
}
|
||||
}
|
||||
sm.disableShortcuts(appShortcutIds, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public void disableAppShortcut(ComputerDetails computer, NvApp app, CharSequence reason) {
|
||||
tvChannelHelper.deleteProgram(computer, app);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
String id = getShortcutIdForGame(computer, app);
|
||||
if (getInfoForId(id) != null) {
|
||||
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void enableAppShortcut(ComputerDetails computer, NvApp app) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
String id = getShortcutIdForGame(computer, app);
|
||||
if (getInfoForId(id) != null) {
|
||||
sm.enableShortcuts(Collections.singletonList(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
345
app/src/main/java/com/limelight/utils/TvChannelHelper.java
Normal file
345
app/src/main/java/com/limelight/utils/TvChannelHelper.java
Normal file
@@ -0,0 +1,345 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.tv.TvContract;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.PosterContentProvider;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class TvChannelHelper {
|
||||
|
||||
private static final int ASPECT_RATIO_MOVIE_POSTER = 5;
|
||||
private static final int TYPE_GAME = 12;
|
||||
private static final int INTERNAL_PROVIDER_ID_INDEX = 1;
|
||||
private static final int PROGRAM_BROWSABLE_INDEX = 2;
|
||||
private static final int ID_INDEX = 0;
|
||||
private Activity context;
|
||||
|
||||
public TvChannelHelper(Activity context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
void requestChannelOnHomeScreen(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(TvContract.ACTION_REQUEST_CHANNEL_BROWSABLE);
|
||||
intent.putExtra(TvContract.EXTRA_CHANNEL_ID, getChannelId(computer.uuid));
|
||||
try {
|
||||
context.startActivityForResult(intent, 0);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createTvChannel(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChannelBuilder builder = new ChannelBuilder()
|
||||
.setType(TvContract.Channels.TYPE_PREVIEW)
|
||||
.setDisplayName(computer.name)
|
||||
.setInternalProviderId(computer.uuid)
|
||||
.setAppLinkIntent(ServerHelper.createPcShortcutIntent(context, computer));
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId != null) {
|
||||
context.getContentResolver().update(TvContract.buildChannelUri(channelId),
|
||||
builder.toContentValues(), null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri channelUri = context.getContentResolver().insert(
|
||||
TvContract.Channels.CONTENT_URI, builder.toContentValues());
|
||||
if (channelUri != null) {
|
||||
long id = ContentUris.parseId(channelUri);
|
||||
updateChannelIcon(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private void updateChannelIcon(long channelId) {
|
||||
Bitmap logo = drawableToBitmap(context.getResources().getDrawable(R.drawable.ic_channel));
|
||||
try {
|
||||
Uri localUri = TvContract.buildChannelLogoUri(channelId);
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) {
|
||||
logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||
outputStream.flush();
|
||||
} catch (SQLiteException | IOException e) {
|
||||
LimeLog.warning("Failed to store the logo to the system content provider.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
logo.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap drawableToBitmap(Drawable drawable) {
|
||||
int width = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
|
||||
int height = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
void addGameToChannel(ComputerDetails computer, NvApp app) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewProgramBuilder builder = new PreviewProgramBuilder()
|
||||
.setChannelId(channelId)
|
||||
.setType(TYPE_GAME)
|
||||
.setTitle(app.getAppName())
|
||||
.setPosterArtAspectRatio(ASPECT_RATIO_MOVIE_POSTER)
|
||||
.setPosterArtUri(PosterContentProvider.createBoxArtUri(computer.uuid, ""+app.getAppId()))
|
||||
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
|
||||
.setInternalProviderId(""+app.getAppId())
|
||||
// Weight should increase each time we run the game
|
||||
.setWeight((int)((System.currentTimeMillis() - 1500000000000L) / 1000));
|
||||
|
||||
Long programId = getProgramId(channelId, ""+app.getAppId());
|
||||
if (programId != null) {
|
||||
context.getContentResolver().update(TvContract.buildPreviewProgramUri(programId),
|
||||
builder.toContentValues(), null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
|
||||
builder.toContentValues());
|
||||
|
||||
TvContract.requestChannelBrowsable(context, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteChannel(ComputerDetails computer) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.getContentResolver().delete(TvContract.buildChannelUri(channelId), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteProgram(ComputerDetails computer, NvApp app) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!isAndroidTV()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long channelId = getChannelId(computer.uuid);
|
||||
if (channelId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Long programId = getProgramId(channelId, ""+app.getAppId());
|
||||
if (programId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.getContentResolver().delete(TvContract.buildPreviewProgramUri(programId), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private Long getChannelId(String computerUuid) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
TvContract.Channels.CONTENT_URI,
|
||||
new String[] {TvContract.Channels._ID, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID},
|
||||
null,
|
||||
null,
|
||||
null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
|
||||
if (computerUuid.equals(internalProviderId)) {
|
||||
return cursor.getLong(ID_INDEX);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private Long getProgramId(long channelId, String appId) {
|
||||
try (Cursor cursor = context.getContentResolver().query(
|
||||
TvContract.buildPreviewProgramsUriForChannel(channelId),
|
||||
new String[] {TvContract.PreviewPrograms._ID, TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, TvContract.PreviewPrograms.COLUMN_BROWSABLE},
|
||||
null,
|
||||
null,
|
||||
null)) {
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
|
||||
if (appId.equals(internalProviderId)) {
|
||||
long id = cursor.getLong(ID_INDEX);
|
||||
int browsable = cursor.getInt(PROGRAM_BROWSABLE_INDEX);
|
||||
if (browsable != 0) {
|
||||
return id;
|
||||
} else {
|
||||
int countDeleted = context.getContentResolver().delete(TvContract.buildPreviewProgramUri(id), null, null);
|
||||
if (countDeleted > 0) {
|
||||
LimeLog.info("Preview program has been deleted");
|
||||
} else {
|
||||
LimeLog.warning("Preview program has not been deleted");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> String toValueString(T value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private static String toUriString(Intent intent) {
|
||||
return intent == null ? null : intent.toUri(Intent.URI_INTENT_SCHEME);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private boolean isAndroidTV() {
|
||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private static class PreviewProgramBuilder {
|
||||
|
||||
private ContentValues mValues = new ContentValues();
|
||||
|
||||
|
||||
public PreviewProgramBuilder setChannelId(Long channelId) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_CHANNEL_ID, channelId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setType(int type) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_TYPE, type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setTitle(String title) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_TITLE, title);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setPosterArtAspectRatio(int aspectRatio) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, aspectRatio);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setIntent(Intent intent) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toUriString(intent));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setIntentUri(Uri uri) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toValueString(uri));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setInternalProviderId(String id) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setPosterArtUri(Uri uri) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_URI, toValueString(uri));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreviewProgramBuilder setWeight(int weight) {
|
||||
mValues.put(TvContract.PreviewPrograms.COLUMN_WEIGHT, weight);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
return new ContentValues(mValues);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private static class ChannelBuilder {
|
||||
|
||||
private ContentValues mValues = new ContentValues();
|
||||
|
||||
public ChannelBuilder setType(String type) {
|
||||
mValues.put(TvContract.Channels.COLUMN_TYPE, type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelBuilder setDisplayName(String displayName) {
|
||||
mValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelBuilder setInternalProviderId(String internalProviderId) {
|
||||
mValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChannelBuilder setAppLinkIntent(Intent intent) {
|
||||
mValues.put(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, toUriString(intent));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
return new ContentValues(mValues);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,22 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Insets;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class UiHelper {
|
||||
|
||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||
private static final int TV_VERTICAL_PADDING_DP = 15;
|
||||
private static final int TV_HORIZONTAL_PADDING_DP = 15;
|
||||
|
||||
public static void setLocale(Activity activity)
|
||||
{
|
||||
@@ -44,22 +46,28 @@ public class UiHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static void notifyNewRootView(Activity activity)
|
||||
public static void applyStatusBarPadding(View view) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// This applies the padding that we omitted in notifyNewRootView() on Q
|
||||
view.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
|
||||
view.setPadding(view.getPaddingLeft(),
|
||||
view.getPaddingTop(),
|
||||
view.getPaddingRight(),
|
||||
windowInsets.getTappableElementInsets().bottom);
|
||||
return windowInsets;
|
||||
}
|
||||
});
|
||||
view.requestApplyInsets();
|
||||
}
|
||||
}
|
||||
|
||||
public static void notifyNewRootView(final Activity activity)
|
||||
{
|
||||
View rootView = activity.findViewById(android.R.id.content);
|
||||
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||
|
||||
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
|
||||
{
|
||||
// Increase view padding on TVs
|
||||
float scale = activity.getResources().getDisplayMetrics().density;
|
||||
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
||||
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
||||
|
||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||
horizontalPaddingPixels, verticalPaddingPixels);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// Allow this non-streaming activity to layout under notches.
|
||||
//
|
||||
@@ -69,6 +77,44 @@ public class UiHelper {
|
||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
|
||||
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
|
||||
// Increase view padding on TVs
|
||||
float scale = activity.getResources().getDisplayMetrics().density;
|
||||
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
|
||||
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
|
||||
|
||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||
horizontalPaddingPixels, verticalPaddingPixels);
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Draw under the status bar on Android Q devices
|
||||
|
||||
// Using getDecorView() here breaks the translucent status/navigation bar when gestures are disabled
|
||||
activity.findViewById(android.R.id.content).setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
|
||||
// Use the tappable insets so we can draw under the status bar in gesture mode
|
||||
Insets tappableInsets = windowInsets.getTappableElementInsets();
|
||||
view.setPadding(tappableInsets.left,
|
||||
tappableInsets.top,
|
||||
tappableInsets.right,
|
||||
0);
|
||||
|
||||
// Show a translucent navigation bar if we can't tap there
|
||||
if (tappableInsets.bottom != 0) {
|
||||
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
else {
|
||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
|
||||
return windowInsets;
|
||||
}
|
||||
});
|
||||
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showDecoderCrashDialog(Activity activity) {
|
||||
@@ -135,4 +181,32 @@ public class UiHelper {
|
||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, 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.delete_pc_msg))
|
||||
.setTitle(computer.name)
|
||||
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
public class Vector2d {
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
58
app/src/main/jni/moonlight-core/Android.mk
Normal file
58
app/src/main/jni/moonlight-core/Android.mk
Normal file
@@ -0,0 +1,58 @@
|
||||
# Android.mk for moonlight-core and binding
|
||||
MY_LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(call all-subdir-makefiles)
|
||||
|
||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := moonlight-core
|
||||
|
||||
LOCAL_SRC_FILES := moonlight-common-c/src/AudioStream.c \
|
||||
moonlight-common-c/src/ByteBuffer.c \
|
||||
moonlight-common-c/src/Connection.c \
|
||||
moonlight-common-c/src/ControlStream.c \
|
||||
moonlight-common-c/src/FakeCallbacks.c \
|
||||
moonlight-common-c/src/InputStream.c \
|
||||
moonlight-common-c/src/LinkedBlockingQueue.c \
|
||||
moonlight-common-c/src/Misc.c \
|
||||
moonlight-common-c/src/Platform.c \
|
||||
moonlight-common-c/src/PlatformSockets.c \
|
||||
moonlight-common-c/src/RtpFecQueue.c \
|
||||
moonlight-common-c/src/RtpReorderQueue.c \
|
||||
moonlight-common-c/src/RtspConnection.c \
|
||||
moonlight-common-c/src/RtspParser.c \
|
||||
moonlight-common-c/src/SdpGenerator.c \
|
||||
moonlight-common-c/src/SimpleStun.c \
|
||||
moonlight-common-c/src/VideoDepacketizer.c \
|
||||
moonlight-common-c/src/VideoStream.c \
|
||||
moonlight-common-c/reedsolomon/rs.c \
|
||||
moonlight-common-c/enet/callbacks.c \
|
||||
moonlight-common-c/enet/compress.c \
|
||||
moonlight-common-c/enet/host.c \
|
||||
moonlight-common-c/enet/list.c \
|
||||
moonlight-common-c/enet/packet.c \
|
||||
moonlight-common-c/enet/peer.c \
|
||||
moonlight-common-c/enet/protocol.c \
|
||||
moonlight-common-c/enet/unix.c \
|
||||
moonlight-common-c/enet/win32.c \
|
||||
simplejni.c \
|
||||
callbacks.c \
|
||||
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/moonlight-common-c/enet/include \
|
||||
$(LOCAL_PATH)/moonlight-common-c/reedsolomon \
|
||||
$(LOCAL_PATH)/moonlight-common-c/src \
|
||||
|
||||
LOCAL_CFLAGS := -DHAS_SOCKLEN_T=1 -DLC_ANDROID -DHAVE_CLOCK_GETTIME=1
|
||||
|
||||
ifeq ($(NDK_DEBUG),1)
|
||||
LOCAL_CFLAGS += -DLC_DEBUG
|
||||
endif
|
||||
|
||||
LOCAL_LDLIBS := -llog
|
||||
|
||||
LOCAL_STATIC_LIBRARIES := libopus libssl libcrypto
|
||||
LOCAL_LDFLAGS += -Wl,--exclude-libs,ALL
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
418
app/src/main/jni/moonlight-core/callbacks.c
Normal file
418
app/src/main/jni/moonlight-core/callbacks.c
Normal file
@@ -0,0 +1,418 @@
|
||||
#include <jni.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <opus_multistream.h>
|
||||
#include <android/log.h>
|
||||
|
||||
static OpusMSDecoder* Decoder;
|
||||
static OPUS_MULTISTREAM_CONFIGURATION OpusConfig;
|
||||
|
||||
static JavaVM *JVM;
|
||||
static pthread_key_t JniEnvKey;
|
||||
static pthread_once_t JniEnvKeyInitOnce = PTHREAD_ONCE_INIT;
|
||||
static jclass GlobalBridgeClass;
|
||||
static jmethodID BridgeDrSetupMethod;
|
||||
static jmethodID BridgeDrStartMethod;
|
||||
static jmethodID BridgeDrStopMethod;
|
||||
static jmethodID BridgeDrCleanupMethod;
|
||||
static jmethodID BridgeDrSubmitDecodeUnitMethod;
|
||||
static jmethodID BridgeArInitMethod;
|
||||
static jmethodID BridgeArStartMethod;
|
||||
static jmethodID BridgeArStopMethod;
|
||||
static jmethodID BridgeArCleanupMethod;
|
||||
static jmethodID BridgeArPlaySampleMethod;
|
||||
static jmethodID BridgeClStageStartingMethod;
|
||||
static jmethodID BridgeClStageCompleteMethod;
|
||||
static jmethodID BridgeClStageFailedMethod;
|
||||
static jmethodID BridgeClConnectionStartedMethod;
|
||||
static jmethodID BridgeClConnectionTerminatedMethod;
|
||||
static jmethodID BridgeClRumbleMethod;
|
||||
static jmethodID BridgeClConnectionStatusUpdateMethod;
|
||||
static jbyteArray DecodedFrameBuffer;
|
||||
static jshortArray DecodedAudioBuffer;
|
||||
|
||||
void DetachThread(void* context) {
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
}
|
||||
|
||||
void JniEnvKeyInit(void) {
|
||||
// Create a TLS slot for the JNIEnv. We aren't in
|
||||
// a pthread during init, so we must wait until we
|
||||
// are to initialize this.
|
||||
pthread_key_create(&JniEnvKey, DetachThread);
|
||||
}
|
||||
|
||||
JNIEnv* GetThreadEnv(void) {
|
||||
JNIEnv* env;
|
||||
|
||||
// First check if this is already attached to the JVM
|
||||
if ((*JVM)->GetEnv(JVM, (void**)&env, JNI_VERSION_1_4) == JNI_OK) {
|
||||
return env;
|
||||
}
|
||||
|
||||
// Create the TLS slot now that we're safely in a pthread
|
||||
pthread_once(&JniEnvKeyInitOnce, JniEnvKeyInit);
|
||||
|
||||
// Try the TLS to see if we already have a JNIEnv
|
||||
env = pthread_getspecific(JniEnvKey);
|
||||
if (env)
|
||||
return env;
|
||||
|
||||
// This is the thread's first JNI call, so attach now
|
||||
(*JVM)->AttachCurrentThread(JVM, &env, NULL);
|
||||
|
||||
// Write our JNIEnv to TLS, so we detach before dying
|
||||
pthread_setspecific(JniEnvKey, env);
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_limelight_nvstream_jni_MoonBridge_init(JNIEnv *env, jclass clazz) {
|
||||
(*env)->GetJavaVM(env, &JVM);
|
||||
GlobalBridgeClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/limelight/nvstream/jni/MoonBridge"));
|
||||
BridgeDrSetupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSetup", "(IIII)I");
|
||||
BridgeDrStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStart", "()V");
|
||||
BridgeDrStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrStop", "()V");
|
||||
BridgeDrCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrCleanup", "()V");
|
||||
BridgeDrSubmitDecodeUnitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeDrSubmitDecodeUnit", "([BIIIJ)I");
|
||||
BridgeArInitMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArInit", "(III)I");
|
||||
BridgeArStartMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStart", "()V");
|
||||
BridgeArStopMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArStop", "()V");
|
||||
BridgeArCleanupMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArCleanup", "()V");
|
||||
BridgeArPlaySampleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeArPlaySample", "([S)V");
|
||||
BridgeClStageStartingMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageStarting", "(I)V");
|
||||
BridgeClStageCompleteMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageComplete", "(I)V");
|
||||
BridgeClStageFailedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClStageFailed", "(II)V");
|
||||
BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V");
|
||||
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(I)V");
|
||||
BridgeClRumbleMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClRumble", "(SSS)V");
|
||||
BridgeClConnectionStatusUpdateMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStatusUpdate", "(I)V");
|
||||
}
|
||||
|
||||
int BridgeDrSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
int err;
|
||||
|
||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSetupMethod, videoFormat, width, height, redrawRate);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// This is called on a Java thread, so it's safe to return
|
||||
return -1;
|
||||
}
|
||||
else if (err != 0) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Use a 32K frame buffer that will increase if needed
|
||||
DecodedFrameBuffer = (*env)->NewGlobalRef(env, (*env)->NewByteArray(env, 32768));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void BridgeDrStart(void) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStartMethod);
|
||||
}
|
||||
|
||||
void BridgeDrStop(void) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStopMethod);
|
||||
}
|
||||
|
||||
void BridgeDrCleanup(void) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrCleanupMethod);
|
||||
}
|
||||
|
||||
int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
int ret;
|
||||
|
||||
// Increase the size of our frame data buffer if our frame won't fit
|
||||
if ((*env)->GetArrayLength(env, DecodedFrameBuffer) < decodeUnit->fullLength) {
|
||||
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
||||
DecodedFrameBuffer = (*env)->NewGlobalRef(env, (*env)->NewByteArray(env, decodeUnit->fullLength));
|
||||
}
|
||||
|
||||
PLENTRY currentEntry;
|
||||
int offset;
|
||||
|
||||
currentEntry = decodeUnit->bufferList;
|
||||
offset = 0;
|
||||
while (currentEntry != NULL) {
|
||||
// Submit parameter set NALUs separately from picture data
|
||||
if (currentEntry->bufferType != BUFFER_TYPE_PICDATA) {
|
||||
// Use the beginning of the buffer each time since this is a separate
|
||||
// invocation of the decoder each time.
|
||||
(*env)->SetByteArrayRegion(env, DecodedFrameBuffer, 0, currentEntry->length, (jbyte*)currentEntry->data);
|
||||
|
||||
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||
DecodedFrameBuffer, currentEntry->length, currentEntry->bufferType,
|
||||
decodeUnit->frameNumber, decodeUnit->receiveTimeMs);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// We will crash here
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
return DR_OK;
|
||||
}
|
||||
else if (ret != DR_OK) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
else {
|
||||
(*env)->SetByteArrayRegion(env, DecodedFrameBuffer, offset, currentEntry->length, (jbyte*)currentEntry->data);
|
||||
offset += currentEntry->length;
|
||||
}
|
||||
|
||||
currentEntry = currentEntry->next;
|
||||
}
|
||||
|
||||
ret = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
|
||||
decodeUnit->frameNumber,
|
||||
decodeUnit->receiveTimeMs);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// We will crash here
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
return DR_OK;
|
||||
}
|
||||
else {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
int err;
|
||||
|
||||
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration, opusConfig->sampleRate, opusConfig->samplesPerFrame);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// This is called on a Java thread, so it's safe to return
|
||||
err = -1;
|
||||
}
|
||||
if (err == 0) {
|
||||
memcpy(&OpusConfig, opusConfig, sizeof(*opusConfig));
|
||||
Decoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||
opusConfig->channelCount,
|
||||
opusConfig->streams,
|
||||
opusConfig->coupledStreams,
|
||||
opusConfig->mapping,
|
||||
&err);
|
||||
if (Decoder == NULL) {
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArCleanupMethod);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We know ahead of time what the buffer size will be for decoded audio, so pre-allocate it
|
||||
DecodedAudioBuffer = (*env)->NewGlobalRef(env, (*env)->NewShortArray(env, opusConfig->channelCount * opusConfig->samplesPerFrame));
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
void BridgeArStart(void) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStartMethod);
|
||||
}
|
||||
|
||||
void BridgeArStop(void) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStopMethod);
|
||||
}
|
||||
|
||||
void BridgeArCleanup() {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
opus_multistream_decoder_destroy(Decoder);
|
||||
|
||||
(*env)->DeleteGlobalRef(env, DecodedAudioBuffer);
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArCleanupMethod);
|
||||
}
|
||||
|
||||
void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
jshort* decodedData = (*env)->GetPrimitiveArrayCritical(env, DecodedAudioBuffer, NULL);
|
||||
|
||||
int decodeLen = opus_multistream_decode(Decoder,
|
||||
(const unsigned char*)sampleData,
|
||||
sampleLength,
|
||||
decodedData,
|
||||
OpusConfig.samplesPerFrame,
|
||||
0);
|
||||
if (decodeLen > 0) {
|
||||
// We must release the array elements before making further JNI calls
|
||||
(*env)->ReleasePrimitiveArrayCritical(env, DecodedAudioBuffer, decodedData, 0);
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArPlaySampleMethod, DecodedAudioBuffer);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// We will crash here
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We can abort here to avoid the copy back since no data was modified
|
||||
(*env)->ReleasePrimitiveArrayCritical(env, DecodedAudioBuffer, decodedData, JNI_ABORT);
|
||||
}
|
||||
}
|
||||
|
||||
void BridgeClStageStarting(int stage) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageStartingMethod, stage);
|
||||
}
|
||||
|
||||
void BridgeClStageComplete(int stage) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage);
|
||||
}
|
||||
|
||||
void BridgeClStageFailed(int stage, int errorCode) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageFailedMethod, stage, errorCode);
|
||||
}
|
||||
|
||||
void BridgeClConnectionStarted(void) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod);
|
||||
}
|
||||
|
||||
void BridgeClConnectionTerminated(int errorCode) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionTerminatedMethod, errorCode);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// We will crash here
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
}
|
||||
}
|
||||
|
||||
void BridgeClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
// The seemingly redundant short casts are required in order to convert the unsigned short to a signed short.
|
||||
// If we leave it as an unsigned short, CheckJNI will fail when the value exceeds 32767. The cast itself is
|
||||
// fine because the Java code treats the value as unsigned even though it's stored in a signed type.
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, (short)lowFreqMotor, (short)highFreqMotor);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// We will crash here
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
}
|
||||
}
|
||||
|
||||
void BridgeClConnectionStatusUpdate(int connectionStatus) {
|
||||
JNIEnv* env = GetThreadEnv();
|
||||
|
||||
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStatusUpdateMethod, connectionStatus);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
// We will crash here
|
||||
(*JVM)->DetachCurrentThread(JVM);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BridgeClLogMessage(const char* format, ...) {
|
||||
va_list va;
|
||||
va_start(va, format);
|
||||
__android_log_vprint(ANDROID_LOG_INFO, "moonlight-common-c", format, va);
|
||||
va_end(va);
|
||||
}
|
||||
|
||||
static DECODER_RENDERER_CALLBACKS BridgeVideoRendererCallbacks = {
|
||||
.setup = BridgeDrSetup,
|
||||
.start = BridgeDrStart,
|
||||
.stop = BridgeDrStop,
|
||||
.cleanup = BridgeDrCleanup,
|
||||
.submitDecodeUnit = BridgeDrSubmitDecodeUnit,
|
||||
};
|
||||
|
||||
static AUDIO_RENDERER_CALLBACKS BridgeAudioRendererCallbacks = {
|
||||
.init = BridgeArInit,
|
||||
.start = BridgeArStart,
|
||||
.stop = BridgeArStop,
|
||||
.cleanup = BridgeArCleanup,
|
||||
.decodeAndPlaySample = BridgeArDecodeAndPlaySample,
|
||||
.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION
|
||||
};
|
||||
|
||||
static CONNECTION_LISTENER_CALLBACKS BridgeConnListenerCallbacks = {
|
||||
.stageStarting = BridgeClStageStarting,
|
||||
.stageComplete = BridgeClStageComplete,
|
||||
.stageFailed = BridgeClStageFailed,
|
||||
.connectionStarted = BridgeClConnectionStarted,
|
||||
.connectionTerminated = BridgeClConnectionTerminated,
|
||||
.logMessage = BridgeClLogMessage,
|
||||
.rumble = BridgeClRumble,
|
||||
.connectionStatusUpdate = BridgeClConnectionStatusUpdate,
|
||||
};
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_nvstream_jni_MoonBridge_startConnection(JNIEnv *env, jclass clazz,
|
||||
jstring address, jstring appVersion, jstring gfeVersion,
|
||||
jint width, jint height, jint fps,
|
||||
jint bitrate, jint packetSize, jint streamingRemotely,
|
||||
jint audioConfiguration, jboolean supportsHevc,
|
||||
jboolean enableHdr,
|
||||
jint hevcBitratePercentageMultiplier,
|
||||
jint clientRefreshRateX100,
|
||||
jbyteArray riAesKey, jbyteArray riAesIv,
|
||||
jint videoCapabilities) {
|
||||
SERVER_INFORMATION serverInfo = {
|
||||
.address = (*env)->GetStringUTFChars(env, address, 0),
|
||||
.serverInfoAppVersion = (*env)->GetStringUTFChars(env, appVersion, 0),
|
||||
.serverInfoGfeVersion = gfeVersion ? (*env)->GetStringUTFChars(env, gfeVersion, 0) : NULL,
|
||||
};
|
||||
STREAM_CONFIGURATION streamConfig = {
|
||||
.width = width,
|
||||
.height = height,
|
||||
.fps = fps,
|
||||
.bitrate = bitrate,
|
||||
.packetSize = packetSize,
|
||||
.streamingRemotely = streamingRemotely,
|
||||
.audioConfiguration = audioConfiguration,
|
||||
.supportsHevc = supportsHevc,
|
||||
.enableHdr = enableHdr,
|
||||
.hevcBitratePercentageMultiplier = hevcBitratePercentageMultiplier,
|
||||
.clientRefreshRateX100 = clientRefreshRateX100
|
||||
};
|
||||
|
||||
jbyte* riAesKeyBuf = (*env)->GetByteArrayElements(env, riAesKey, NULL);
|
||||
memcpy(streamConfig.remoteInputAesKey, riAesKeyBuf, sizeof(streamConfig.remoteInputAesKey));
|
||||
(*env)->ReleaseByteArrayElements(env, riAesKey, riAesKeyBuf, JNI_ABORT);
|
||||
|
||||
jbyte* riAesIvBuf = (*env)->GetByteArrayElements(env, riAesIv, NULL);
|
||||
memcpy(streamConfig.remoteInputAesIv, riAesIvBuf, sizeof(streamConfig.remoteInputAesIv));
|
||||
(*env)->ReleaseByteArrayElements(env, riAesIv, riAesIvBuf, JNI_ABORT);
|
||||
|
||||
BridgeVideoRendererCallbacks.capabilities = videoCapabilities;
|
||||
|
||||
int ret = LiStartConnection(&serverInfo,
|
||||
&streamConfig,
|
||||
&BridgeConnListenerCallbacks,
|
||||
&BridgeVideoRendererCallbacks,
|
||||
&BridgeAudioRendererCallbacks,
|
||||
NULL, 0,
|
||||
NULL, 0);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, address, serverInfo.address);
|
||||
(*env)->ReleaseStringUTFChars(env, appVersion, serverInfo.serverInfoAppVersion);
|
||||
if (gfeVersion != NULL) {
|
||||
(*env)->ReleaseStringUTFChars(env, gfeVersion, serverInfo.serverInfoGfeVersion);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
7
app/src/main/jni/moonlight-core/libopus/Android.mk
Normal file
7
app/src/main/jni/moonlight-core/libopus/Android.mk
Normal file
@@ -0,0 +1,7 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libopus
|
||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/libopus.a
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
include $(PREBUILT_STATIC_LIBRARY)
|
||||
BIN
app/src/main/jni/moonlight-core/libopus/arm64-v8a/libopus.a
Normal file
BIN
app/src/main/jni/moonlight-core/libopus/arm64-v8a/libopus.a
Normal file
Binary file not shown.
BIN
app/src/main/jni/moonlight-core/libopus/armeabi-v7a/libopus.a
Normal file
BIN
app/src/main/jni/moonlight-core/libopus/armeabi-v7a/libopus.a
Normal file
Binary file not shown.
100
app/src/main/jni/moonlight-core/libopus/build_android.sh
Normal file
100
app/src/main/jni/moonlight-core/libopus/build_android.sh
Normal file
@@ -0,0 +1,100 @@
|
||||
ANDROID_API_TARGET=21
|
||||
PARALLEL_JOBS=$(nproc)
|
||||
|
||||
rm -r ./android
|
||||
mkdir android
|
||||
|
||||
function build_one
|
||||
{
|
||||
PREFIX=$(pwd)/android/$CPU
|
||||
SYSROOT=$NDK/platforms/android-$ANDROID_API_TARGET/arch-$SYSROOT_CPU
|
||||
TOOLCHAIN_PATH=$NDK/toolchains/$TOOLCHAIN_DIR/prebuilt/linux-x86_64
|
||||
export PATH=$PATH:$TOOLCHAIN_PATH/bin
|
||||
./configure \
|
||||
--build=x86_64-unknown-linux-gnu \
|
||||
--host=$TOOLCHAIN_BIN_PREFIX \
|
||||
--target=$TOOLCHAIN_BIN_PREFIX \
|
||||
CFLAGS="--sysroot=$SYSROOT -O2 $ADDI_CFLAGS" \
|
||||
$ADDI_CONFIGURE_FLAGS
|
||||
make clean
|
||||
make -j$PARALLEL_JOBS
|
||||
mkdir android/$CPU
|
||||
cp .libs/libopus.a android/$CPU
|
||||
}
|
||||
|
||||
function build_mips
|
||||
{
|
||||
CPU=mips
|
||||
SYSROOT_CPU=mips
|
||||
TOOLCHAIN_BIN_PREFIX=mipsel-linux-android
|
||||
TOOLCHAIN_DIR=mipsel-linux-android-4.9
|
||||
ADDI_CFLAGS="-mips32 -mhard-float -EL -mno-dsp"
|
||||
ADDI_CONFIGURE_FLAGS="--enable-fixed-point" # fixed point
|
||||
build_one
|
||||
}
|
||||
|
||||
function build_mips64
|
||||
{
|
||||
CPU=mips64
|
||||
SYSROOT_CPU=mips64
|
||||
TOOLCHAIN_BIN_PREFIX=mips64el-linux-android
|
||||
TOOLCHAIN_DIR=mips64el-linux-android-4.9
|
||||
ADDI_CFLAGS="-mips64r6"
|
||||
ADDI_CONFIGURE_FLAGS="--enable-fixed-point" # fixed point
|
||||
build_one
|
||||
}
|
||||
|
||||
function build_x86
|
||||
{
|
||||
CPU=x86
|
||||
SYSROOT_CPU=x86
|
||||
TOOLCHAIN_BIN_PREFIX=i686-linux-android
|
||||
TOOLCHAIN_DIR=x86-4.9
|
||||
ADDI_CFLAGS="-march=i686 -mtune=atom -mstackrealign -msse -msse2 -msse3 -mssse3 -mfpmath=sse -m32"
|
||||
ADDI_CONFIGURE_FLAGS="" # floating point for SSE optimizations
|
||||
build_one
|
||||
}
|
||||
|
||||
function build_x86_64
|
||||
{
|
||||
CPU=x86_64
|
||||
SYSROOT_CPU=x86_64
|
||||
TOOLCHAIN_BIN_PREFIX=x86_64-linux-android
|
||||
TOOLCHAIN_DIR=x86_64-4.9
|
||||
ADDI_CFLAGS="-msse -msse2 -msse3 -mssse3 -msse4 -msse4.1 -msse4.2 -mpopcnt -m64"
|
||||
ADDI_CONFIGURE_FLAGS="" # floating point for SSE optimizations
|
||||
build_one
|
||||
}
|
||||
|
||||
function build_armv7
|
||||
{
|
||||
CPU=arm
|
||||
SYSROOT_CPU=arm
|
||||
TOOLCHAIN_BIN_PREFIX=arm-linux-androideabi
|
||||
TOOLCHAIN_DIR=arm-linux-androideabi-4.9
|
||||
ADDI_CFLAGS="-marm -mfpu=vfpv3-d16"
|
||||
ADDI_LDFLAGS=""
|
||||
ADDI_CONFIGURE_FLAGS="--enable-fixed-point" # fixed point for NEON, EDSP, Media
|
||||
build_one
|
||||
}
|
||||
|
||||
# ARMv8 doesn't currently have assembly in the opus project. We still use fixed point
|
||||
# anyway in the hopes that it will be more performant even without assembly.
|
||||
function build_armv8
|
||||
{
|
||||
CPU=aarch64
|
||||
SYSROOT_CPU=arm64
|
||||
TOOLCHAIN_BIN_PREFIX=aarch64-linux-android
|
||||
TOOLCHAIN_DIR=aarch64-linux-android-4.9
|
||||
ADDI_CFLAGS=""
|
||||
ADDI_LDFLAGS=""
|
||||
ADDI_CONFIGURE_FLAGS="--enable-fixed-point"
|
||||
build_one
|
||||
}
|
||||
|
||||
build_mips
|
||||
build_mips64
|
||||
build_x86
|
||||
build_x86_64
|
||||
build_armv7
|
||||
build_armv8
|
||||
981
app/src/main/jni/moonlight-core/libopus/include/opus.h
Normal file
981
app/src/main/jni/moonlight-core/libopus/include/opus.h
Normal file
@@ -0,0 +1,981 @@
|
||||
/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited
|
||||
Written by Jean-Marc Valin and Koen Vos */
|
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file opus.h
|
||||
* @brief Opus reference implementation API
|
||||
*/
|
||||
|
||||
#ifndef OPUS_H
|
||||
#define OPUS_H
|
||||
|
||||
#include "opus_types.h"
|
||||
#include "opus_defines.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @mainpage Opus
|
||||
*
|
||||
* The Opus codec is designed for interactive speech and audio transmission over the Internet.
|
||||
* It is designed by the IETF Codec Working Group and incorporates technology from
|
||||
* Skype's SILK codec and Xiph.Org's CELT codec.
|
||||
*
|
||||
* The Opus codec is designed to handle a wide range of interactive audio applications,
|
||||
* including Voice over IP, videoconferencing, in-game chat, and even remote live music
|
||||
* performances. It can scale from low bit-rate narrowband speech to very high quality
|
||||
* stereo music. Its main features are:
|
||||
|
||||
* @li Sampling rates from 8 to 48 kHz
|
||||
* @li Bit-rates from 6 kb/s to 510 kb/s
|
||||
* @li Support for both constant bit-rate (CBR) and variable bit-rate (VBR)
|
||||
* @li Audio bandwidth from narrowband to full-band
|
||||
* @li Support for speech and music
|
||||
* @li Support for mono and stereo
|
||||
* @li Support for multichannel (up to 255 channels)
|
||||
* @li Frame sizes from 2.5 ms to 60 ms
|
||||
* @li Good loss robustness and packet loss concealment (PLC)
|
||||
* @li Floating point and fixed-point implementation
|
||||
*
|
||||
* Documentation sections:
|
||||
* @li @ref opus_encoder
|
||||
* @li @ref opus_decoder
|
||||
* @li @ref opus_repacketizer
|
||||
* @li @ref opus_multistream
|
||||
* @li @ref opus_libinfo
|
||||
* @li @ref opus_custom
|
||||
*/
|
||||
|
||||
/** @defgroup opus_encoder Opus Encoder
|
||||
* @{
|
||||
*
|
||||
* @brief This page describes the process and functions used to encode Opus.
|
||||
*
|
||||
* Since Opus is a stateful codec, the encoding process starts with creating an encoder
|
||||
* state. This can be done with:
|
||||
*
|
||||
* @code
|
||||
* int error;
|
||||
* OpusEncoder *enc;
|
||||
* enc = opus_encoder_create(Fs, channels, application, &error);
|
||||
* @endcode
|
||||
*
|
||||
* From this point, @c enc can be used for encoding an audio stream. An encoder state
|
||||
* @b must @b not be used for more than one stream at the same time. Similarly, the encoder
|
||||
* state @b must @b not be re-initialized for each frame.
|
||||
*
|
||||
* While opus_encoder_create() allocates memory for the state, it's also possible
|
||||
* to initialize pre-allocated memory:
|
||||
*
|
||||
* @code
|
||||
* int size;
|
||||
* int error;
|
||||
* OpusEncoder *enc;
|
||||
* size = opus_encoder_get_size(channels);
|
||||
* enc = malloc(size);
|
||||
* error = opus_encoder_init(enc, Fs, channels, application);
|
||||
* @endcode
|
||||
*
|
||||
* where opus_encoder_get_size() returns the required size for the encoder state. Note that
|
||||
* future versions of this code may change the size, so no assuptions should be made about it.
|
||||
*
|
||||
* The encoder state is always continuous in memory and only a shallow copy is sufficient
|
||||
* to copy it (e.g. memcpy())
|
||||
*
|
||||
* It is possible to change some of the encoder's settings using the opus_encoder_ctl()
|
||||
* interface. All these settings already default to the recommended value, so they should
|
||||
* only be changed when necessary. The most common settings one may want to change are:
|
||||
*
|
||||
* @code
|
||||
* opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate));
|
||||
* opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
|
||||
* opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type));
|
||||
* @endcode
|
||||
*
|
||||
* where
|
||||
*
|
||||
* @arg bitrate is in bits per second (b/s)
|
||||
* @arg complexity is a value from 1 to 10, where 1 is the lowest complexity and 10 is the highest
|
||||
* @arg signal_type is either OPUS_AUTO (default), OPUS_SIGNAL_VOICE, or OPUS_SIGNAL_MUSIC
|
||||
*
|
||||
* See @ref opus_encoderctls and @ref opus_genericctls for a complete list of parameters that can be set or queried. Most parameters can be set or changed at any time during a stream.
|
||||
*
|
||||
* To encode a frame, opus_encode() or opus_encode_float() must be called with exactly one frame (2.5, 5, 10, 20, 40 or 60 ms) of audio data:
|
||||
* @code
|
||||
* len = opus_encode(enc, audio_frame, frame_size, packet, max_packet);
|
||||
* @endcode
|
||||
*
|
||||
* where
|
||||
* <ul>
|
||||
* <li>audio_frame is the audio data in opus_int16 (or float for opus_encode_float())</li>
|
||||
* <li>frame_size is the duration of the frame in samples (per channel)</li>
|
||||
* <li>packet is the byte array to which the compressed data is written</li>
|
||||
* <li>max_packet is the maximum number of bytes that can be written in the packet (4000 bytes is recommended).
|
||||
* Do not use max_packet to control VBR target bitrate, instead use the #OPUS_SET_BITRATE CTL.</li>
|
||||
* </ul>
|
||||
*
|
||||
* opus_encode() and opus_encode_float() return the number of bytes actually written to the packet.
|
||||
* The return value <b>can be negative</b>, which indicates that an error has occurred. If the return value
|
||||
* is 2 bytes or less, then the packet does not need to be transmitted (DTX).
|
||||
*
|
||||
* Once the encoder state if no longer needed, it can be destroyed with
|
||||
*
|
||||
* @code
|
||||
* opus_encoder_destroy(enc);
|
||||
* @endcode
|
||||
*
|
||||
* If the encoder was created with opus_encoder_init() rather than opus_encoder_create(),
|
||||
* then no action is required aside from potentially freeing the memory that was manually
|
||||
* allocated for it (calling free(enc) for the example above)
|
||||
*
|
||||
*/
|
||||
|
||||
/** Opus encoder state.
|
||||
* This contains the complete state of an Opus encoder.
|
||||
* It is position independent and can be freely copied.
|
||||
* @see opus_encoder_create,opus_encoder_init
|
||||
*/
|
||||
typedef struct OpusEncoder OpusEncoder;
|
||||
|
||||
/** Gets the size of an <code>OpusEncoder</code> structure.
|
||||
* @param[in] channels <tt>int</tt>: Number of channels.
|
||||
* This must be 1 or 2.
|
||||
* @returns The size in bytes.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_encoder_get_size(int channels);
|
||||
|
||||
/**
|
||||
*/
|
||||
|
||||
/** Allocates and initializes an encoder state.
|
||||
* There are three coding modes:
|
||||
*
|
||||
* @ref OPUS_APPLICATION_VOIP gives best quality at a given bitrate for voice
|
||||
* signals. It enhances the input signal by high-pass filtering and
|
||||
* emphasizing formants and harmonics. Optionally it includes in-band
|
||||
* forward error correction to protect against packet loss. Use this
|
||||
* mode for typical VoIP applications. Because of the enhancement,
|
||||
* even at high bitrates the output may sound different from the input.
|
||||
*
|
||||
* @ref OPUS_APPLICATION_AUDIO gives best quality at a given bitrate for most
|
||||
* non-voice signals like music. Use this mode for music and mixed
|
||||
* (music/voice) content, broadcast, and applications requiring less
|
||||
* than 15 ms of coding delay.
|
||||
*
|
||||
* @ref OPUS_APPLICATION_RESTRICTED_LOWDELAY configures low-delay mode that
|
||||
* disables the speech-optimized mode in exchange for slightly reduced delay.
|
||||
* This mode can only be set on an newly initialized or freshly reset encoder
|
||||
* because it changes the codec delay.
|
||||
*
|
||||
* This is useful when the caller knows that the speech-optimized modes will not be needed (use with caution).
|
||||
* @param [in] Fs <tt>opus_int32</tt>: Sampling rate of input signal (Hz)
|
||||
* This must be one of 8000, 12000, 16000,
|
||||
* 24000, or 48000.
|
||||
* @param [in] channels <tt>int</tt>: Number of channels (1 or 2) in input signal
|
||||
* @param [in] application <tt>int</tt>: Coding mode (@ref OPUS_APPLICATION_VOIP/@ref OPUS_APPLICATION_AUDIO/@ref OPUS_APPLICATION_RESTRICTED_LOWDELAY)
|
||||
* @param [out] error <tt>int*</tt>: @ref opus_errorcodes
|
||||
* @note Regardless of the sampling rate and number channels selected, the Opus encoder
|
||||
* can switch to a lower audio bandwidth or number of channels if the bitrate
|
||||
* selected is too low. This also means that it is safe to always use 48 kHz stereo input
|
||||
* and let the encoder optimize the encoding.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create(
|
||||
opus_int32 Fs,
|
||||
int channels,
|
||||
int application,
|
||||
int *error
|
||||
);
|
||||
|
||||
/** Initializes a previously allocated encoder state
|
||||
* The memory pointed to by st must be at least the size returned by opus_encoder_get_size().
|
||||
* This is intended for applications which use their own allocator instead of malloc.
|
||||
* @see opus_encoder_create(),opus_encoder_get_size()
|
||||
* To reset a previously initialized state, use the #OPUS_RESET_STATE CTL.
|
||||
* @param [in] st <tt>OpusEncoder*</tt>: Encoder state
|
||||
* @param [in] Fs <tt>opus_int32</tt>: Sampling rate of input signal (Hz)
|
||||
* This must be one of 8000, 12000, 16000,
|
||||
* 24000, or 48000.
|
||||
* @param [in] channels <tt>int</tt>: Number of channels (1 or 2) in input signal
|
||||
* @param [in] application <tt>int</tt>: Coding mode (OPUS_APPLICATION_VOIP/OPUS_APPLICATION_AUDIO/OPUS_APPLICATION_RESTRICTED_LOWDELAY)
|
||||
* @retval #OPUS_OK Success or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_EXPORT int opus_encoder_init(
|
||||
OpusEncoder *st,
|
||||
opus_int32 Fs,
|
||||
int channels,
|
||||
int application
|
||||
) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Encodes an Opus frame.
|
||||
* @param [in] st <tt>OpusEncoder*</tt>: Encoder state
|
||||
* @param [in] pcm <tt>opus_int16*</tt>: Input signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16)
|
||||
* @param [in] frame_size <tt>int</tt>: Number of samples per channel in the
|
||||
* input signal.
|
||||
* This must be an Opus frame size for
|
||||
* the encoder's sampling rate.
|
||||
* For example, at 48 kHz the permitted
|
||||
* values are 120, 240, 480, 960, 1920,
|
||||
* and 2880.
|
||||
* Passing in a duration of less than
|
||||
* 10 ms (480 samples at 48 kHz) will
|
||||
* prevent the encoder from using the LPC
|
||||
* or hybrid modes.
|
||||
* @param [out] data <tt>unsigned char*</tt>: Output payload.
|
||||
* This must contain storage for at
|
||||
* least \a max_data_bytes.
|
||||
* @param [in] max_data_bytes <tt>opus_int32</tt>: Size of the allocated
|
||||
* memory for the output
|
||||
* payload. This may be
|
||||
* used to impose an upper limit on
|
||||
* the instant bitrate, but should
|
||||
* not be used as the only bitrate
|
||||
* control. Use #OPUS_SET_BITRATE to
|
||||
* control the bitrate.
|
||||
* @returns The length of the encoded packet (in bytes) on success or a
|
||||
* negative error code (see @ref opus_errorcodes) on failure.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode(
|
||||
OpusEncoder *st,
|
||||
const opus_int16 *pcm,
|
||||
int frame_size,
|
||||
unsigned char *data,
|
||||
opus_int32 max_data_bytes
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Encodes an Opus frame from floating point input.
|
||||
* @param [in] st <tt>OpusEncoder*</tt>: Encoder state
|
||||
* @param [in] pcm <tt>float*</tt>: Input in float format (interleaved if 2 channels), with a normal range of +/-1.0.
|
||||
* Samples with a range beyond +/-1.0 are supported but will
|
||||
* be clipped by decoders using the integer API and should
|
||||
* only be used if it is known that the far end supports
|
||||
* extended dynamic range.
|
||||
* length is frame_size*channels*sizeof(float)
|
||||
* @param [in] frame_size <tt>int</tt>: Number of samples per channel in the
|
||||
* input signal.
|
||||
* This must be an Opus frame size for
|
||||
* the encoder's sampling rate.
|
||||
* For example, at 48 kHz the permitted
|
||||
* values are 120, 240, 480, 960, 1920,
|
||||
* and 2880.
|
||||
* Passing in a duration of less than
|
||||
* 10 ms (480 samples at 48 kHz) will
|
||||
* prevent the encoder from using the LPC
|
||||
* or hybrid modes.
|
||||
* @param [out] data <tt>unsigned char*</tt>: Output payload.
|
||||
* This must contain storage for at
|
||||
* least \a max_data_bytes.
|
||||
* @param [in] max_data_bytes <tt>opus_int32</tt>: Size of the allocated
|
||||
* memory for the output
|
||||
* payload. This may be
|
||||
* used to impose an upper limit on
|
||||
* the instant bitrate, but should
|
||||
* not be used as the only bitrate
|
||||
* control. Use #OPUS_SET_BITRATE to
|
||||
* control the bitrate.
|
||||
* @returns The length of the encoded packet (in bytes) on success or a
|
||||
* negative error code (see @ref opus_errorcodes) on failure.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode_float(
|
||||
OpusEncoder *st,
|
||||
const float *pcm,
|
||||
int frame_size,
|
||||
unsigned char *data,
|
||||
opus_int32 max_data_bytes
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Frees an <code>OpusEncoder</code> allocated by opus_encoder_create().
|
||||
* @param[in] st <tt>OpusEncoder*</tt>: State to be freed.
|
||||
*/
|
||||
OPUS_EXPORT void opus_encoder_destroy(OpusEncoder *st);
|
||||
|
||||
/** Perform a CTL function on an Opus encoder.
|
||||
*
|
||||
* Generally the request and subsequent arguments are generated
|
||||
* by a convenience macro.
|
||||
* @param st <tt>OpusEncoder*</tt>: Encoder state.
|
||||
* @param request This and all remaining parameters should be replaced by one
|
||||
* of the convenience macros in @ref opus_genericctls or
|
||||
* @ref opus_encoderctls.
|
||||
* @see opus_genericctls
|
||||
* @see opus_encoderctls
|
||||
*/
|
||||
OPUS_EXPORT int opus_encoder_ctl(OpusEncoder *st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||
/**@}*/
|
||||
|
||||
/** @defgroup opus_decoder Opus Decoder
|
||||
* @{
|
||||
*
|
||||
* @brief This page describes the process and functions used to decode Opus.
|
||||
*
|
||||
* The decoding process also starts with creating a decoder
|
||||
* state. This can be done with:
|
||||
* @code
|
||||
* int error;
|
||||
* OpusDecoder *dec;
|
||||
* dec = opus_decoder_create(Fs, channels, &error);
|
||||
* @endcode
|
||||
* where
|
||||
* @li Fs is the sampling rate and must be 8000, 12000, 16000, 24000, or 48000
|
||||
* @li channels is the number of channels (1 or 2)
|
||||
* @li error will hold the error code in case of failure (or #OPUS_OK on success)
|
||||
* @li the return value is a newly created decoder state to be used for decoding
|
||||
*
|
||||
* While opus_decoder_create() allocates memory for the state, it's also possible
|
||||
* to initialize pre-allocated memory:
|
||||
* @code
|
||||
* int size;
|
||||
* int error;
|
||||
* OpusDecoder *dec;
|
||||
* size = opus_decoder_get_size(channels);
|
||||
* dec = malloc(size);
|
||||
* error = opus_decoder_init(dec, Fs, channels);
|
||||
* @endcode
|
||||
* where opus_decoder_get_size() returns the required size for the decoder state. Note that
|
||||
* future versions of this code may change the size, so no assuptions should be made about it.
|
||||
*
|
||||
* The decoder state is always continuous in memory and only a shallow copy is sufficient
|
||||
* to copy it (e.g. memcpy())
|
||||
*
|
||||
* To decode a frame, opus_decode() or opus_decode_float() must be called with a packet of compressed audio data:
|
||||
* @code
|
||||
* frame_size = opus_decode(dec, packet, len, decoded, max_size, 0);
|
||||
* @endcode
|
||||
* where
|
||||
*
|
||||
* @li packet is the byte array containing the compressed data
|
||||
* @li len is the exact number of bytes contained in the packet
|
||||
* @li decoded is the decoded audio data in opus_int16 (or float for opus_decode_float())
|
||||
* @li max_size is the max duration of the frame in samples (per channel) that can fit into the decoded_frame array
|
||||
*
|
||||
* opus_decode() and opus_decode_float() return the number of samples (per channel) decoded from the packet.
|
||||
* If that value is negative, then an error has occurred. This can occur if the packet is corrupted or if the audio
|
||||
* buffer is too small to hold the decoded audio.
|
||||
*
|
||||
* Opus is a stateful codec with overlapping blocks and as a result Opus
|
||||
* packets are not coded independently of each other. Packets must be
|
||||
* passed into the decoder serially and in the correct order for a correct
|
||||
* decode. Lost packets can be replaced with loss concealment by calling
|
||||
* the decoder with a null pointer and zero length for the missing packet.
|
||||
*
|
||||
* A single codec state may only be accessed from a single thread at
|
||||
* a time and any required locking must be performed by the caller. Separate
|
||||
* streams must be decoded with separate decoder states and can be decoded
|
||||
* in parallel unless the library was compiled with NONTHREADSAFE_PSEUDOSTACK
|
||||
* defined.
|
||||
*
|
||||
*/
|
||||
|
||||
/** Opus decoder state.
|
||||
* This contains the complete state of an Opus decoder.
|
||||
* It is position independent and can be freely copied.
|
||||
* @see opus_decoder_create,opus_decoder_init
|
||||
*/
|
||||
typedef struct OpusDecoder OpusDecoder;
|
||||
|
||||
/** Gets the size of an <code>OpusDecoder</code> structure.
|
||||
* @param [in] channels <tt>int</tt>: Number of channels.
|
||||
* This must be 1 or 2.
|
||||
* @returns The size in bytes.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_size(int channels);
|
||||
|
||||
/** Allocates and initializes a decoder state.
|
||||
* @param [in] Fs <tt>opus_int32</tt>: Sample rate to decode at (Hz).
|
||||
* This must be one of 8000, 12000, 16000,
|
||||
* 24000, or 48000.
|
||||
* @param [in] channels <tt>int</tt>: Number of channels (1 or 2) to decode
|
||||
* @param [out] error <tt>int*</tt>: #OPUS_OK Success or @ref opus_errorcodes
|
||||
*
|
||||
* Internally Opus stores data at 48000 Hz, so that should be the default
|
||||
* value for Fs. However, the decoder can efficiently decode to buffers
|
||||
* at 8, 12, 16, and 24 kHz so if for some reason the caller cannot use
|
||||
* data at the full sample rate, or knows the compressed data doesn't
|
||||
* use the full frequency range, it can request decoding at a reduced
|
||||
* rate. Likewise, the decoder is capable of filling in either mono or
|
||||
* interleaved stereo pcm buffers, at the caller's request.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusDecoder *opus_decoder_create(
|
||||
opus_int32 Fs,
|
||||
int channels,
|
||||
int *error
|
||||
);
|
||||
|
||||
/** Initializes a previously allocated decoder state.
|
||||
* The state must be at least the size returned by opus_decoder_get_size().
|
||||
* This is intended for applications which use their own allocator instead of malloc. @see opus_decoder_create,opus_decoder_get_size
|
||||
* To reset a previously initialized state, use the #OPUS_RESET_STATE CTL.
|
||||
* @param [in] st <tt>OpusDecoder*</tt>: Decoder state.
|
||||
* @param [in] Fs <tt>opus_int32</tt>: Sampling rate to decode to (Hz).
|
||||
* This must be one of 8000, 12000, 16000,
|
||||
* 24000, or 48000.
|
||||
* @param [in] channels <tt>int</tt>: Number of channels (1 or 2) to decode
|
||||
* @retval #OPUS_OK Success or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_EXPORT int opus_decoder_init(
|
||||
OpusDecoder *st,
|
||||
opus_int32 Fs,
|
||||
int channels
|
||||
) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Decode an Opus packet.
|
||||
* @param [in] st <tt>OpusDecoder*</tt>: Decoder state
|
||||
* @param [in] data <tt>char*</tt>: Input payload. Use a NULL pointer to indicate packet loss
|
||||
* @param [in] len <tt>opus_int32</tt>: Number of bytes in payload*
|
||||
* @param [out] pcm <tt>opus_int16*</tt>: Output signal (interleaved if 2 channels). length
|
||||
* is frame_size*channels*sizeof(opus_int16)
|
||||
* @param [in] frame_size Number of samples per channel of available space in \a pcm.
|
||||
* If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will
|
||||
* not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1),
|
||||
* then frame_size needs to be exactly the duration of audio that is missing, otherwise the
|
||||
* decoder will not be in the optimal state to decode the next incoming packet. For the PLC and
|
||||
* FEC cases, frame_size <b>must</b> be a multiple of 2.5 ms.
|
||||
* @param [in] decode_fec <tt>int</tt>: Flag (0 or 1) to request that any in-band forward error correction data be
|
||||
* decoded. If no such data is available, the frame is decoded as if it were lost.
|
||||
* @returns Number of decoded samples or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode(
|
||||
OpusDecoder *st,
|
||||
const unsigned char *data,
|
||||
opus_int32 len,
|
||||
opus_int16 *pcm,
|
||||
int frame_size,
|
||||
int decode_fec
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Decode an Opus packet with floating point output.
|
||||
* @param [in] st <tt>OpusDecoder*</tt>: Decoder state
|
||||
* @param [in] data <tt>char*</tt>: Input payload. Use a NULL pointer to indicate packet loss
|
||||
* @param [in] len <tt>opus_int32</tt>: Number of bytes in payload
|
||||
* @param [out] pcm <tt>float*</tt>: Output signal (interleaved if 2 channels). length
|
||||
* is frame_size*channels*sizeof(float)
|
||||
* @param [in] frame_size Number of samples per channel of available space in \a pcm.
|
||||
* If this is less than the maximum packet duration (120ms; 5760 for 48kHz), this function will
|
||||
* not be capable of decoding some packets. In the case of PLC (data==NULL) or FEC (decode_fec=1),
|
||||
* then frame_size needs to be exactly the duration of audio that is missing, otherwise the
|
||||
* decoder will not be in the optimal state to decode the next incoming packet. For the PLC and
|
||||
* FEC cases, frame_size <b>must</b> be a multiple of 2.5 ms.
|
||||
* @param [in] decode_fec <tt>int</tt>: Flag (0 or 1) to request that any in-band forward error correction data be
|
||||
* decoded. If no such data is available the frame is decoded as if it were lost.
|
||||
* @returns Number of decoded samples or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decode_float(
|
||||
OpusDecoder *st,
|
||||
const unsigned char *data,
|
||||
opus_int32 len,
|
||||
float *pcm,
|
||||
int frame_size,
|
||||
int decode_fec
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Perform a CTL function on an Opus decoder.
|
||||
*
|
||||
* Generally the request and subsequent arguments are generated
|
||||
* by a convenience macro.
|
||||
* @param st <tt>OpusDecoder*</tt>: Decoder state.
|
||||
* @param request This and all remaining parameters should be replaced by one
|
||||
* of the convenience macros in @ref opus_genericctls or
|
||||
* @ref opus_decoderctls.
|
||||
* @see opus_genericctls
|
||||
* @see opus_decoderctls
|
||||
*/
|
||||
OPUS_EXPORT int opus_decoder_ctl(OpusDecoder *st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Frees an <code>OpusDecoder</code> allocated by opus_decoder_create().
|
||||
* @param[in] st <tt>OpusDecoder*</tt>: State to be freed.
|
||||
*/
|
||||
OPUS_EXPORT void opus_decoder_destroy(OpusDecoder *st);
|
||||
|
||||
/** Parse an opus packet into one or more frames.
|
||||
* Opus_decode will perform this operation internally so most applications do
|
||||
* not need to use this function.
|
||||
* This function does not copy the frames, the returned pointers are pointers into
|
||||
* the input packet.
|
||||
* @param [in] data <tt>char*</tt>: Opus packet to be parsed
|
||||
* @param [in] len <tt>opus_int32</tt>: size of data
|
||||
* @param [out] out_toc <tt>char*</tt>: TOC pointer
|
||||
* @param [out] frames <tt>char*[48]</tt> encapsulated frames
|
||||
* @param [out] size <tt>opus_int16[48]</tt> sizes of the encapsulated frames
|
||||
* @param [out] payload_offset <tt>int*</tt>: returns the position of the payload within the packet (in bytes)
|
||||
* @returns number of frames
|
||||
*/
|
||||
OPUS_EXPORT int opus_packet_parse(
|
||||
const unsigned char *data,
|
||||
opus_int32 len,
|
||||
unsigned char *out_toc,
|
||||
const unsigned char *frames[48],
|
||||
opus_int16 size[48],
|
||||
int *payload_offset
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5);
|
||||
|
||||
/** Gets the bandwidth of an Opus packet.
|
||||
* @param [in] data <tt>char*</tt>: Opus packet
|
||||
* @retval OPUS_BANDWIDTH_NARROWBAND Narrowband (4kHz bandpass)
|
||||
* @retval OPUS_BANDWIDTH_MEDIUMBAND Mediumband (6kHz bandpass)
|
||||
* @retval OPUS_BANDWIDTH_WIDEBAND Wideband (8kHz bandpass)
|
||||
* @retval OPUS_BANDWIDTH_SUPERWIDEBAND Superwideband (12kHz bandpass)
|
||||
* @retval OPUS_BANDWIDTH_FULLBAND Fullband (20kHz bandpass)
|
||||
* @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_bandwidth(const unsigned char *data) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Gets the number of samples per frame from an Opus packet.
|
||||
* @param [in] data <tt>char*</tt>: Opus packet.
|
||||
* This must contain at least one byte of
|
||||
* data.
|
||||
* @param [in] Fs <tt>opus_int32</tt>: Sampling rate in Hz.
|
||||
* This must be a multiple of 400, or
|
||||
* inaccurate results will be returned.
|
||||
* @returns Number of samples per frame.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_samples_per_frame(const unsigned char *data, opus_int32 Fs) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Gets the number of channels from an Opus packet.
|
||||
* @param [in] data <tt>char*</tt>: Opus packet
|
||||
* @returns Number of channels
|
||||
* @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_channels(const unsigned char *data) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Gets the number of frames in an Opus packet.
|
||||
* @param [in] packet <tt>char*</tt>: Opus packet
|
||||
* @param [in] len <tt>opus_int32</tt>: Length of packet
|
||||
* @returns Number of frames
|
||||
* @retval OPUS_BAD_ARG Insufficient data was passed to the function
|
||||
* @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_frames(const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Gets the number of samples of an Opus packet.
|
||||
* @param [in] packet <tt>char*</tt>: Opus packet
|
||||
* @param [in] len <tt>opus_int32</tt>: Length of packet
|
||||
* @param [in] Fs <tt>opus_int32</tt>: Sampling rate in Hz.
|
||||
* This must be a multiple of 400, or
|
||||
* inaccurate results will be returned.
|
||||
* @returns Number of samples
|
||||
* @retval OPUS_BAD_ARG Insufficient data was passed to the function
|
||||
* @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_packet_get_nb_samples(const unsigned char packet[], opus_int32 len, opus_int32 Fs) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Gets the number of samples of an Opus packet.
|
||||
* @param [in] dec <tt>OpusDecoder*</tt>: Decoder state
|
||||
* @param [in] packet <tt>char*</tt>: Opus packet
|
||||
* @param [in] len <tt>opus_int32</tt>: Length of packet
|
||||
* @returns Number of samples
|
||||
* @retval OPUS_BAD_ARG Insufficient data was passed to the function
|
||||
* @retval OPUS_INVALID_PACKET The compressed data passed is corrupted or of an unsupported type
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_decoder_get_nb_samples(const OpusDecoder *dec, const unsigned char packet[], opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2);
|
||||
|
||||
/** Applies soft-clipping to bring a float signal within the [-1,1] range. If
|
||||
* the signal is already in that range, nothing is done. If there are values
|
||||
* outside of [-1,1], then the signal is clipped as smoothly as possible to
|
||||
* both fit in the range and avoid creating excessive distortion in the
|
||||
* process.
|
||||
* @param [in,out] pcm <tt>float*</tt>: Input PCM and modified PCM
|
||||
* @param [in] frame_size <tt>int</tt> Number of samples per channel to process
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @param [in,out] softclip_mem <tt>float*</tt>: State memory for the soft clipping process (one float per channel, initialized to zero)
|
||||
*/
|
||||
OPUS_EXPORT void opus_pcm_soft_clip(float *pcm, int frame_size, int channels, float *softclip_mem);
|
||||
|
||||
|
||||
/**@}*/
|
||||
|
||||
/** @defgroup opus_repacketizer Repacketizer
|
||||
* @{
|
||||
*
|
||||
* The repacketizer can be used to merge multiple Opus packets into a single
|
||||
* packet or alternatively to split Opus packets that have previously been
|
||||
* merged. Splitting valid Opus packets is always guaranteed to succeed,
|
||||
* whereas merging valid packets only succeeds if all frames have the same
|
||||
* mode, bandwidth, and frame size, and when the total duration of the merged
|
||||
* packet is no more than 120 ms. The 120 ms limit comes from the
|
||||
* specification and limits decoder memory requirements at a point where
|
||||
* framing overhead becomes negligible.
|
||||
*
|
||||
* The repacketizer currently only operates on elementary Opus
|
||||
* streams. It will not manipualte multistream packets successfully, except in
|
||||
* the degenerate case where they consist of data from a single stream.
|
||||
*
|
||||
* The repacketizing process starts with creating a repacketizer state, either
|
||||
* by calling opus_repacketizer_create() or by allocating the memory yourself,
|
||||
* e.g.,
|
||||
* @code
|
||||
* OpusRepacketizer *rp;
|
||||
* rp = (OpusRepacketizer*)malloc(opus_repacketizer_get_size());
|
||||
* if (rp != NULL)
|
||||
* opus_repacketizer_init(rp);
|
||||
* @endcode
|
||||
*
|
||||
* Then the application should submit packets with opus_repacketizer_cat(),
|
||||
* extract new packets with opus_repacketizer_out() or
|
||||
* opus_repacketizer_out_range(), and then reset the state for the next set of
|
||||
* input packets via opus_repacketizer_init().
|
||||
*
|
||||
* For example, to split a sequence of packets into individual frames:
|
||||
* @code
|
||||
* unsigned char *data;
|
||||
* int len;
|
||||
* while (get_next_packet(&data, &len))
|
||||
* {
|
||||
* unsigned char out[1276];
|
||||
* opus_int32 out_len;
|
||||
* int nb_frames;
|
||||
* int err;
|
||||
* int i;
|
||||
* err = opus_repacketizer_cat(rp, data, len);
|
||||
* if (err != OPUS_OK)
|
||||
* {
|
||||
* release_packet(data);
|
||||
* return err;
|
||||
* }
|
||||
* nb_frames = opus_repacketizer_get_nb_frames(rp);
|
||||
* for (i = 0; i < nb_frames; i++)
|
||||
* {
|
||||
* out_len = opus_repacketizer_out_range(rp, i, i+1, out, sizeof(out));
|
||||
* if (out_len < 0)
|
||||
* {
|
||||
* release_packet(data);
|
||||
* return (int)out_len;
|
||||
* }
|
||||
* output_next_packet(out, out_len);
|
||||
* }
|
||||
* opus_repacketizer_init(rp);
|
||||
* release_packet(data);
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* Alternatively, to combine a sequence of frames into packets that each
|
||||
* contain up to <code>TARGET_DURATION_MS</code> milliseconds of data:
|
||||
* @code
|
||||
* // The maximum number of packets with duration TARGET_DURATION_MS occurs
|
||||
* // when the frame size is 2.5 ms, for a total of (TARGET_DURATION_MS*2/5)
|
||||
* // packets.
|
||||
* unsigned char *data[(TARGET_DURATION_MS*2/5)+1];
|
||||
* opus_int32 len[(TARGET_DURATION_MS*2/5)+1];
|
||||
* int nb_packets;
|
||||
* unsigned char out[1277*(TARGET_DURATION_MS*2/2)];
|
||||
* opus_int32 out_len;
|
||||
* int prev_toc;
|
||||
* nb_packets = 0;
|
||||
* while (get_next_packet(data+nb_packets, len+nb_packets))
|
||||
* {
|
||||
* int nb_frames;
|
||||
* int err;
|
||||
* nb_frames = opus_packet_get_nb_frames(data[nb_packets], len[nb_packets]);
|
||||
* if (nb_frames < 1)
|
||||
* {
|
||||
* release_packets(data, nb_packets+1);
|
||||
* return nb_frames;
|
||||
* }
|
||||
* nb_frames += opus_repacketizer_get_nb_frames(rp);
|
||||
* // If adding the next packet would exceed our target, or it has an
|
||||
* // incompatible TOC sequence, output the packets we already have before
|
||||
* // submitting it.
|
||||
* // N.B., The nb_packets > 0 check ensures we've submitted at least one
|
||||
* // packet since the last call to opus_repacketizer_init(). Otherwise a
|
||||
* // single packet longer than TARGET_DURATION_MS would cause us to try to
|
||||
* // output an (invalid) empty packet. It also ensures that prev_toc has
|
||||
* // been set to a valid value. Additionally, len[nb_packets] > 0 is
|
||||
* // guaranteed by the call to opus_packet_get_nb_frames() above, so the
|
||||
* // reference to data[nb_packets][0] should be valid.
|
||||
* if (nb_packets > 0 && (
|
||||
* ((prev_toc & 0xFC) != (data[nb_packets][0] & 0xFC)) ||
|
||||
* opus_packet_get_samples_per_frame(data[nb_packets], 48000)*nb_frames >
|
||||
* TARGET_DURATION_MS*48))
|
||||
* {
|
||||
* out_len = opus_repacketizer_out(rp, out, sizeof(out));
|
||||
* if (out_len < 0)
|
||||
* {
|
||||
* release_packets(data, nb_packets+1);
|
||||
* return (int)out_len;
|
||||
* }
|
||||
* output_next_packet(out, out_len);
|
||||
* opus_repacketizer_init(rp);
|
||||
* release_packets(data, nb_packets);
|
||||
* data[0] = data[nb_packets];
|
||||
* len[0] = len[nb_packets];
|
||||
* nb_packets = 0;
|
||||
* }
|
||||
* err = opus_repacketizer_cat(rp, data[nb_packets], len[nb_packets]);
|
||||
* if (err != OPUS_OK)
|
||||
* {
|
||||
* release_packets(data, nb_packets+1);
|
||||
* return err;
|
||||
* }
|
||||
* prev_toc = data[nb_packets][0];
|
||||
* nb_packets++;
|
||||
* }
|
||||
* // Output the final, partial packet.
|
||||
* if (nb_packets > 0)
|
||||
* {
|
||||
* out_len = opus_repacketizer_out(rp, out, sizeof(out));
|
||||
* release_packets(data, nb_packets);
|
||||
* if (out_len < 0)
|
||||
* return (int)out_len;
|
||||
* output_next_packet(out, out_len);
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* An alternate way of merging packets is to simply call opus_repacketizer_cat()
|
||||
* unconditionally until it fails. At that point, the merged packet can be
|
||||
* obtained with opus_repacketizer_out() and the input packet for which
|
||||
* opus_repacketizer_cat() needs to be re-added to a newly reinitialized
|
||||
* repacketizer state.
|
||||
*/
|
||||
|
||||
typedef struct OpusRepacketizer OpusRepacketizer;
|
||||
|
||||
/** Gets the size of an <code>OpusRepacketizer</code> structure.
|
||||
* @returns The size in bytes.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_size(void);
|
||||
|
||||
/** (Re)initializes a previously allocated repacketizer state.
|
||||
* The state must be at least the size returned by opus_repacketizer_get_size().
|
||||
* This can be used for applications which use their own allocator instead of
|
||||
* malloc().
|
||||
* It must also be called to reset the queue of packets waiting to be
|
||||
* repacketized, which is necessary if the maximum packet duration of 120 ms
|
||||
* is reached or if you wish to submit packets with a different Opus
|
||||
* configuration (coding mode, audio bandwidth, frame size, or channel count).
|
||||
* Failure to do so will prevent a new packet from being added with
|
||||
* opus_repacketizer_cat().
|
||||
* @see opus_repacketizer_create
|
||||
* @see opus_repacketizer_get_size
|
||||
* @see opus_repacketizer_cat
|
||||
* @param rp <tt>OpusRepacketizer*</tt>: The repacketizer state to
|
||||
* (re)initialize.
|
||||
* @returns A pointer to the same repacketizer state that was passed in.
|
||||
*/
|
||||
OPUS_EXPORT OpusRepacketizer *opus_repacketizer_init(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Allocates memory and initializes the new repacketizer with
|
||||
* opus_repacketizer_init().
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusRepacketizer *opus_repacketizer_create(void);
|
||||
|
||||
/** Frees an <code>OpusRepacketizer</code> allocated by
|
||||
* opus_repacketizer_create().
|
||||
* @param[in] rp <tt>OpusRepacketizer*</tt>: State to be freed.
|
||||
*/
|
||||
OPUS_EXPORT void opus_repacketizer_destroy(OpusRepacketizer *rp);
|
||||
|
||||
/** Add a packet to the current repacketizer state.
|
||||
* This packet must match the configuration of any packets already submitted
|
||||
* for repacketization since the last call to opus_repacketizer_init().
|
||||
* This means that it must have the same coding mode, audio bandwidth, frame
|
||||
* size, and channel count.
|
||||
* This can be checked in advance by examining the top 6 bits of the first
|
||||
* byte of the packet, and ensuring they match the top 6 bits of the first
|
||||
* byte of any previously submitted packet.
|
||||
* The total duration of audio in the repacketizer state also must not exceed
|
||||
* 120 ms, the maximum duration of a single packet, after adding this packet.
|
||||
*
|
||||
* The contents of the current repacketizer state can be extracted into new
|
||||
* packets using opus_repacketizer_out() or opus_repacketizer_out_range().
|
||||
*
|
||||
* In order to add a packet with a different configuration or to add more
|
||||
* audio beyond 120 ms, you must clear the repacketizer state by calling
|
||||
* opus_repacketizer_init().
|
||||
* If a packet is too large to add to the current repacketizer state, no part
|
||||
* of it is added, even if it contains multiple frames, some of which might
|
||||
* fit.
|
||||
* If you wish to be able to add parts of such packets, you should first use
|
||||
* another repacketizer to split the packet into pieces and add them
|
||||
* individually.
|
||||
* @see opus_repacketizer_out_range
|
||||
* @see opus_repacketizer_out
|
||||
* @see opus_repacketizer_init
|
||||
* @param rp <tt>OpusRepacketizer*</tt>: The repacketizer state to which to
|
||||
* add the packet.
|
||||
* @param[in] data <tt>const unsigned char*</tt>: The packet data.
|
||||
* The application must ensure
|
||||
* this pointer remains valid
|
||||
* until the next call to
|
||||
* opus_repacketizer_init() or
|
||||
* opus_repacketizer_destroy().
|
||||
* @param len <tt>opus_int32</tt>: The number of bytes in the packet data.
|
||||
* @returns An error code indicating whether or not the operation succeeded.
|
||||
* @retval #OPUS_OK The packet's contents have been added to the repacketizer
|
||||
* state.
|
||||
* @retval #OPUS_INVALID_PACKET The packet did not have a valid TOC sequence,
|
||||
* the packet's TOC sequence was not compatible
|
||||
* with previously submitted packets (because
|
||||
* the coding mode, audio bandwidth, frame size,
|
||||
* or channel count did not match), or adding
|
||||
* this packet would increase the total amount of
|
||||
* audio stored in the repacketizer state to more
|
||||
* than 120 ms.
|
||||
*/
|
||||
OPUS_EXPORT int opus_repacketizer_cat(OpusRepacketizer *rp, const unsigned char *data, opus_int32 len) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2);
|
||||
|
||||
|
||||
/** Construct a new packet from data previously submitted to the repacketizer
|
||||
* state via opus_repacketizer_cat().
|
||||
* @param rp <tt>OpusRepacketizer*</tt>: The repacketizer state from which to
|
||||
* construct the new packet.
|
||||
* @param begin <tt>int</tt>: The index of the first frame in the current
|
||||
* repacketizer state to include in the output.
|
||||
* @param end <tt>int</tt>: One past the index of the last frame in the
|
||||
* current repacketizer state to include in the
|
||||
* output.
|
||||
* @param[out] data <tt>const unsigned char*</tt>: The buffer in which to
|
||||
* store the output packet.
|
||||
* @param maxlen <tt>opus_int32</tt>: The maximum number of bytes to store in
|
||||
* the output buffer. In order to guarantee
|
||||
* success, this should be at least
|
||||
* <code>1276</code> for a single frame,
|
||||
* or for multiple frames,
|
||||
* <code>1277*(end-begin)</code>.
|
||||
* However, <code>1*(end-begin)</code> plus
|
||||
* the size of all packet data submitted to
|
||||
* the repacketizer since the last call to
|
||||
* opus_repacketizer_init() or
|
||||
* opus_repacketizer_create() is also
|
||||
* sufficient, and possibly much smaller.
|
||||
* @returns The total size of the output packet on success, or an error code
|
||||
* on failure.
|
||||
* @retval #OPUS_BAD_ARG <code>[begin,end)</code> was an invalid range of
|
||||
* frames (begin < 0, begin >= end, or end >
|
||||
* opus_repacketizer_get_nb_frames()).
|
||||
* @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the
|
||||
* complete output packet.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out_range(OpusRepacketizer *rp, int begin, int end, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Return the total number of frames contained in packet data submitted to
|
||||
* the repacketizer state so far via opus_repacketizer_cat() since the last
|
||||
* call to opus_repacketizer_init() or opus_repacketizer_create().
|
||||
* This defines the valid range of packets that can be extracted with
|
||||
* opus_repacketizer_out_range() or opus_repacketizer_out().
|
||||
* @param rp <tt>OpusRepacketizer*</tt>: The repacketizer state containing the
|
||||
* frames.
|
||||
* @returns The total number of frames contained in the packet data submitted
|
||||
* to the repacketizer state.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_repacketizer_get_nb_frames(OpusRepacketizer *rp) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Construct a new packet from data previously submitted to the repacketizer
|
||||
* state via opus_repacketizer_cat().
|
||||
* This is a convenience routine that returns all the data submitted so far
|
||||
* in a single packet.
|
||||
* It is equivalent to calling
|
||||
* @code
|
||||
* opus_repacketizer_out_range(rp, 0, opus_repacketizer_get_nb_frames(rp),
|
||||
* data, maxlen)
|
||||
* @endcode
|
||||
* @param rp <tt>OpusRepacketizer*</tt>: The repacketizer state from which to
|
||||
* construct the new packet.
|
||||
* @param[out] data <tt>const unsigned char*</tt>: The buffer in which to
|
||||
* store the output packet.
|
||||
* @param maxlen <tt>opus_int32</tt>: The maximum number of bytes to store in
|
||||
* the output buffer. In order to guarantee
|
||||
* success, this should be at least
|
||||
* <code>1277*opus_repacketizer_get_nb_frames(rp)</code>.
|
||||
* However,
|
||||
* <code>1*opus_repacketizer_get_nb_frames(rp)</code>
|
||||
* plus the size of all packet data
|
||||
* submitted to the repacketizer since the
|
||||
* last call to opus_repacketizer_init() or
|
||||
* opus_repacketizer_create() is also
|
||||
* sufficient, and possibly much smaller.
|
||||
* @returns The total size of the output packet on success, or an error code
|
||||
* on failure.
|
||||
* @retval #OPUS_BUFFER_TOO_SMALL \a maxlen was insufficient to contain the
|
||||
* complete output packet.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_repacketizer_out(OpusRepacketizer *rp, unsigned char *data, opus_int32 maxlen) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Pads a given Opus packet to a larger size (possibly changing the TOC sequence).
|
||||
* @param[in,out] data <tt>const unsigned char*</tt>: The buffer containing the
|
||||
* packet to pad.
|
||||
* @param len <tt>opus_int32</tt>: The size of the packet.
|
||||
* This must be at least 1.
|
||||
* @param new_len <tt>opus_int32</tt>: The desired size of the packet after padding.
|
||||
* This must be at least as large as len.
|
||||
* @returns an error code
|
||||
* @retval #OPUS_OK \a on success.
|
||||
* @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len.
|
||||
* @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet.
|
||||
*/
|
||||
OPUS_EXPORT int opus_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len);
|
||||
|
||||
/** Remove all padding from a given Opus packet and rewrite the TOC sequence to
|
||||
* minimize space usage.
|
||||
* @param[in,out] data <tt>const unsigned char*</tt>: The buffer containing the
|
||||
* packet to strip.
|
||||
* @param len <tt>opus_int32</tt>: The size of the packet.
|
||||
* This must be at least 1.
|
||||
* @returns The new size of the output packet on success, or an error code
|
||||
* on failure.
|
||||
* @retval #OPUS_BAD_ARG \a len was less than 1.
|
||||
* @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_packet_unpad(unsigned char *data, opus_int32 len);
|
||||
|
||||
/** Pads a given Opus multi-stream packet to a larger size (possibly changing the TOC sequence).
|
||||
* @param[in,out] data <tt>const unsigned char*</tt>: The buffer containing the
|
||||
* packet to pad.
|
||||
* @param len <tt>opus_int32</tt>: The size of the packet.
|
||||
* This must be at least 1.
|
||||
* @param new_len <tt>opus_int32</tt>: The desired size of the packet after padding.
|
||||
* This must be at least 1.
|
||||
* @param nb_streams <tt>opus_int32</tt>: The number of streams (not channels) in the packet.
|
||||
* This must be at least as large as len.
|
||||
* @returns an error code
|
||||
* @retval #OPUS_OK \a on success.
|
||||
* @retval #OPUS_BAD_ARG \a len was less than 1.
|
||||
* @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet.
|
||||
*/
|
||||
OPUS_EXPORT int opus_multistream_packet_pad(unsigned char *data, opus_int32 len, opus_int32 new_len, int nb_streams);
|
||||
|
||||
/** Remove all padding from a given Opus multi-stream packet and rewrite the TOC sequence to
|
||||
* minimize space usage.
|
||||
* @param[in,out] data <tt>const unsigned char*</tt>: The buffer containing the
|
||||
* packet to strip.
|
||||
* @param len <tt>opus_int32</tt>: The size of the packet.
|
||||
* This must be at least 1.
|
||||
* @param nb_streams <tt>opus_int32</tt>: The number of streams (not channels) in the packet.
|
||||
* This must be at least 1.
|
||||
* @returns The new size of the output packet on success, or an error code
|
||||
* on failure.
|
||||
* @retval #OPUS_BAD_ARG \a len was less than 1 or new_len was less than len.
|
||||
* @retval #OPUS_INVALID_PACKET \a data did not contain a valid Opus packet.
|
||||
*/
|
||||
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_packet_unpad(unsigned char *data, opus_int32 len, int nb_streams);
|
||||
|
||||
/**@}*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OPUS_H */
|
||||
342
app/src/main/jni/moonlight-core/libopus/include/opus_custom.h
Normal file
342
app/src/main/jni/moonlight-core/libopus/include/opus_custom.h
Normal file
@@ -0,0 +1,342 @@
|
||||
/* Copyright (c) 2007-2008 CSIRO
|
||||
Copyright (c) 2007-2009 Xiph.Org Foundation
|
||||
Copyright (c) 2008-2012 Gregory Maxwell
|
||||
Written by Jean-Marc Valin and Gregory Maxwell */
|
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/**
|
||||
@file opus_custom.h
|
||||
@brief Opus-Custom reference implementation API
|
||||
*/
|
||||
|
||||
#ifndef OPUS_CUSTOM_H
|
||||
#define OPUS_CUSTOM_H
|
||||
|
||||
#include "opus_defines.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef CUSTOM_MODES
|
||||
# define OPUS_CUSTOM_EXPORT OPUS_EXPORT
|
||||
# define OPUS_CUSTOM_EXPORT_STATIC OPUS_EXPORT
|
||||
#else
|
||||
# define OPUS_CUSTOM_EXPORT
|
||||
# ifdef OPUS_BUILD
|
||||
# define OPUS_CUSTOM_EXPORT_STATIC static OPUS_INLINE
|
||||
# else
|
||||
# define OPUS_CUSTOM_EXPORT_STATIC
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/** @defgroup opus_custom Opus Custom
|
||||
* @{
|
||||
* Opus Custom is an optional part of the Opus specification and
|
||||
* reference implementation which uses a distinct API from the regular
|
||||
* API and supports frame sizes that are not normally supported.\ Use
|
||||
* of Opus Custom is discouraged for all but very special applications
|
||||
* for which a frame size different from 2.5, 5, 10, or 20 ms is needed
|
||||
* (for either complexity or latency reasons) and where interoperability
|
||||
* is less important.
|
||||
*
|
||||
* In addition to the interoperability limitations the use of Opus custom
|
||||
* disables a substantial chunk of the codec and generally lowers the
|
||||
* quality available at a given bitrate. Normally when an application needs
|
||||
* a different frame size from the codec it should buffer to match the
|
||||
* sizes but this adds a small amount of delay which may be important
|
||||
* in some very low latency applications. Some transports (especially
|
||||
* constant rate RF transports) may also work best with frames of
|
||||
* particular durations.
|
||||
*
|
||||
* Libopus only supports custom modes if they are enabled at compile time.
|
||||
*
|
||||
* The Opus Custom API is similar to the regular API but the
|
||||
* @ref opus_encoder_create and @ref opus_decoder_create calls take
|
||||
* an additional mode parameter which is a structure produced by
|
||||
* a call to @ref opus_custom_mode_create. Both the encoder and decoder
|
||||
* must create a mode using the same sample rate (fs) and frame size
|
||||
* (frame size) so these parameters must either be signaled out of band
|
||||
* or fixed in a particular implementation.
|
||||
*
|
||||
* Similar to regular Opus the custom modes support on the fly frame size
|
||||
* switching, but the sizes available depend on the particular frame size in
|
||||
* use. For some initial frame sizes on a single on the fly size is available.
|
||||
*/
|
||||
|
||||
/** Contains the state of an encoder. One encoder state is needed
|
||||
for each stream. It is initialized once at the beginning of the
|
||||
stream. Do *not* re-initialize the state for every frame.
|
||||
@brief Encoder state
|
||||
*/
|
||||
typedef struct OpusCustomEncoder OpusCustomEncoder;
|
||||
|
||||
/** State of the decoder. One decoder state is needed for each stream.
|
||||
It is initialized once at the beginning of the stream. Do *not*
|
||||
re-initialize the state for every frame.
|
||||
@brief Decoder state
|
||||
*/
|
||||
typedef struct OpusCustomDecoder OpusCustomDecoder;
|
||||
|
||||
/** The mode contains all the information necessary to create an
|
||||
encoder. Both the encoder and decoder need to be initialized
|
||||
with exactly the same mode, otherwise the output will be
|
||||
corrupted.
|
||||
@brief Mode configuration
|
||||
*/
|
||||
typedef struct OpusCustomMode OpusCustomMode;
|
||||
|
||||
/** Creates a new mode struct. This will be passed to an encoder or
|
||||
* decoder. The mode MUST NOT BE DESTROYED until the encoders and
|
||||
* decoders that use it are destroyed as well.
|
||||
* @param [in] Fs <tt>int</tt>: Sampling rate (8000 to 96000 Hz)
|
||||
* @param [in] frame_size <tt>int</tt>: Number of samples (per channel) to encode in each
|
||||
* packet (64 - 1024, prime factorization must contain zero or more 2s, 3s, or 5s and no other primes)
|
||||
* @param [out] error <tt>int*</tt>: Returned error code (if NULL, no error will be returned)
|
||||
* @return A newly created mode
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomMode *opus_custom_mode_create(opus_int32 Fs, int frame_size, int *error);
|
||||
|
||||
/** Destroys a mode struct. Only call this after all encoders and
|
||||
* decoders using this mode are destroyed as well.
|
||||
* @param [in] mode <tt>OpusCustomMode*</tt>: Mode to be freed.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT void opus_custom_mode_destroy(OpusCustomMode *mode);
|
||||
|
||||
|
||||
#if !defined(OPUS_BUILD) || defined(CELT_ENCODER_C)
|
||||
|
||||
/* Encoder */
|
||||
/** Gets the size of an OpusCustomEncoder structure.
|
||||
* @param [in] mode <tt>OpusCustomMode *</tt>: Mode configuration
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @returns size
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_encoder_get_size(
|
||||
const OpusCustomMode *mode,
|
||||
int channels
|
||||
) OPUS_ARG_NONNULL(1);
|
||||
|
||||
# ifdef CUSTOM_MODES
|
||||
/** Initializes a previously allocated encoder state
|
||||
* The memory pointed to by st must be the size returned by opus_custom_encoder_get_size.
|
||||
* This is intended for applications which use their own allocator instead of malloc.
|
||||
* @see opus_custom_encoder_create(),opus_custom_encoder_get_size()
|
||||
* To reset a previously initialized state use the OPUS_RESET_STATE CTL.
|
||||
* @param [in] st <tt>OpusCustomEncoder*</tt>: Encoder state
|
||||
* @param [in] mode <tt>OpusCustomMode *</tt>: Contains all the information about the characteristics of
|
||||
* the stream (must be the same characteristics as used for the
|
||||
* decoder)
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @return OPUS_OK Success or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT int opus_custom_encoder_init(
|
||||
OpusCustomEncoder *st,
|
||||
const OpusCustomMode *mode,
|
||||
int channels
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2);
|
||||
# endif
|
||||
#endif
|
||||
|
||||
|
||||
/** Creates a new encoder state. Each stream needs its own encoder
|
||||
* state (can't be shared across simultaneous streams).
|
||||
* @param [in] mode <tt>OpusCustomMode*</tt>: Contains all the information about the characteristics of
|
||||
* the stream (must be the same characteristics as used for the
|
||||
* decoder)
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @param [out] error <tt>int*</tt>: Returns an error code
|
||||
* @return Newly created encoder state.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomEncoder *opus_custom_encoder_create(
|
||||
const OpusCustomMode *mode,
|
||||
int channels,
|
||||
int *error
|
||||
) OPUS_ARG_NONNULL(1);
|
||||
|
||||
|
||||
/** Destroys a an encoder state.
|
||||
* @param[in] st <tt>OpusCustomEncoder*</tt>: State to be freed.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT void opus_custom_encoder_destroy(OpusCustomEncoder *st);
|
||||
|
||||
/** Encodes a frame of audio.
|
||||
* @param [in] st <tt>OpusCustomEncoder*</tt>: Encoder state
|
||||
* @param [in] pcm <tt>float*</tt>: PCM audio in float format, with a normal range of +/-1.0.
|
||||
* Samples with a range beyond +/-1.0 are supported but will
|
||||
* be clipped by decoders using the integer API and should
|
||||
* only be used if it is known that the far end supports
|
||||
* extended dynamic range. There must be exactly
|
||||
* frame_size samples per channel.
|
||||
* @param [in] frame_size <tt>int</tt>: Number of samples per frame of input signal
|
||||
* @param [out] compressed <tt>char *</tt>: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long.
|
||||
* @param [in] maxCompressedBytes <tt>int</tt>: Maximum number of bytes to use for compressing the frame
|
||||
* (can change from one frame to another)
|
||||
* @return Number of bytes written to "compressed".
|
||||
* If negative, an error has occurred (see error codes). It is IMPORTANT that
|
||||
* the length returned be somehow transmitted to the decoder. Otherwise, no
|
||||
* decoding is possible.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode_float(
|
||||
OpusCustomEncoder *st,
|
||||
const float *pcm,
|
||||
int frame_size,
|
||||
unsigned char *compressed,
|
||||
int maxCompressedBytes
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Encodes a frame of audio.
|
||||
* @param [in] st <tt>OpusCustomEncoder*</tt>: Encoder state
|
||||
* @param [in] pcm <tt>opus_int16*</tt>: PCM audio in signed 16-bit format (native endian).
|
||||
* There must be exactly frame_size samples per channel.
|
||||
* @param [in] frame_size <tt>int</tt>: Number of samples per frame of input signal
|
||||
* @param [out] compressed <tt>char *</tt>: The compressed data is written here. This may not alias pcm and must be at least maxCompressedBytes long.
|
||||
* @param [in] maxCompressedBytes <tt>int</tt>: Maximum number of bytes to use for compressing the frame
|
||||
* (can change from one frame to another)
|
||||
* @return Number of bytes written to "compressed".
|
||||
* If negative, an error has occurred (see error codes). It is IMPORTANT that
|
||||
* the length returned be somehow transmitted to the decoder. Otherwise, no
|
||||
* decoding is possible.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_encode(
|
||||
OpusCustomEncoder *st,
|
||||
const opus_int16 *pcm,
|
||||
int frame_size,
|
||||
unsigned char *compressed,
|
||||
int maxCompressedBytes
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Perform a CTL function on an Opus custom encoder.
|
||||
*
|
||||
* Generally the request and subsequent arguments are generated
|
||||
* by a convenience macro.
|
||||
* @see opus_encoderctls
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT int opus_custom_encoder_ctl(OpusCustomEncoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||
|
||||
|
||||
#if !defined(OPUS_BUILD) || defined(CELT_DECODER_C)
|
||||
/* Decoder */
|
||||
|
||||
/** Gets the size of an OpusCustomDecoder structure.
|
||||
* @param [in] mode <tt>OpusCustomMode *</tt>: Mode configuration
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @returns size
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT_STATIC OPUS_WARN_UNUSED_RESULT int opus_custom_decoder_get_size(
|
||||
const OpusCustomMode *mode,
|
||||
int channels
|
||||
) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Initializes a previously allocated decoder state
|
||||
* The memory pointed to by st must be the size returned by opus_custom_decoder_get_size.
|
||||
* This is intended for applications which use their own allocator instead of malloc.
|
||||
* @see opus_custom_decoder_create(),opus_custom_decoder_get_size()
|
||||
* To reset a previously initialized state use the OPUS_RESET_STATE CTL.
|
||||
* @param [in] st <tt>OpusCustomDecoder*</tt>: Decoder state
|
||||
* @param [in] mode <tt>OpusCustomMode *</tt>: Contains all the information about the characteristics of
|
||||
* the stream (must be the same characteristics as used for the
|
||||
* encoder)
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @return OPUS_OK Success or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT_STATIC int opus_custom_decoder_init(
|
||||
OpusCustomDecoder *st,
|
||||
const OpusCustomMode *mode,
|
||||
int channels
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/** Creates a new decoder state. Each stream needs its own decoder state (can't
|
||||
* be shared across simultaneous streams).
|
||||
* @param [in] mode <tt>OpusCustomMode</tt>: Contains all the information about the characteristics of the
|
||||
* stream (must be the same characteristics as used for the encoder)
|
||||
* @param [in] channels <tt>int</tt>: Number of channels
|
||||
* @param [out] error <tt>int*</tt>: Returns an error code
|
||||
* @return Newly created decoder state.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT OpusCustomDecoder *opus_custom_decoder_create(
|
||||
const OpusCustomMode *mode,
|
||||
int channels,
|
||||
int *error
|
||||
) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/** Destroys a an decoder state.
|
||||
* @param[in] st <tt>OpusCustomDecoder*</tt>: State to be freed.
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT void opus_custom_decoder_destroy(OpusCustomDecoder *st);
|
||||
|
||||
/** Decode an opus custom frame with floating point output
|
||||
* @param [in] st <tt>OpusCustomDecoder*</tt>: Decoder state
|
||||
* @param [in] data <tt>char*</tt>: Input payload. Use a NULL pointer to indicate packet loss
|
||||
* @param [in] len <tt>int</tt>: Number of bytes in payload
|
||||
* @param [out] pcm <tt>float*</tt>: Output signal (interleaved if 2 channels). length
|
||||
* is frame_size*channels*sizeof(float)
|
||||
* @param [in] frame_size Number of samples per channel of available space in *pcm.
|
||||
* @returns Number of decoded samples or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode_float(
|
||||
OpusCustomDecoder *st,
|
||||
const unsigned char *data,
|
||||
int len,
|
||||
float *pcm,
|
||||
int frame_size
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Decode an opus custom frame
|
||||
* @param [in] st <tt>OpusCustomDecoder*</tt>: Decoder state
|
||||
* @param [in] data <tt>char*</tt>: Input payload. Use a NULL pointer to indicate packet loss
|
||||
* @param [in] len <tt>int</tt>: Number of bytes in payload
|
||||
* @param [out] pcm <tt>opus_int16*</tt>: Output signal (interleaved if 2 channels). length
|
||||
* is frame_size*channels*sizeof(opus_int16)
|
||||
* @param [in] frame_size Number of samples per channel of available space in *pcm.
|
||||
* @returns Number of decoded samples or @ref opus_errorcodes
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT OPUS_WARN_UNUSED_RESULT int opus_custom_decode(
|
||||
OpusCustomDecoder *st,
|
||||
const unsigned char *data,
|
||||
int len,
|
||||
opus_int16 *pcm,
|
||||
int frame_size
|
||||
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4);
|
||||
|
||||
/** Perform a CTL function on an Opus custom decoder.
|
||||
*
|
||||
* Generally the request and subsequent arguments are generated
|
||||
* by a convenience macro.
|
||||
* @see opus_genericctls
|
||||
*/
|
||||
OPUS_CUSTOM_EXPORT int opus_custom_decoder_ctl(OpusCustomDecoder * OPUS_RESTRICT st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||
|
||||
/**@}*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OPUS_CUSTOM_H */
|
||||
788
app/src/main/jni/moonlight-core/libopus/include/opus_defines.h
Normal file
788
app/src/main/jni/moonlight-core/libopus/include/opus_defines.h
Normal file
@@ -0,0 +1,788 @@
|
||||
/* Copyright (c) 2010-2011 Xiph.Org Foundation, Skype Limited
|
||||
Written by Jean-Marc Valin and Koen Vos */
|
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file opus_defines.h
|
||||
* @brief Opus reference implementation constants
|
||||
*/
|
||||
|
||||
#ifndef OPUS_DEFINES_H
|
||||
#define OPUS_DEFINES_H
|
||||
|
||||
#include "opus_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @defgroup opus_errorcodes Error codes
|
||||
* @{
|
||||
*/
|
||||
/** No error @hideinitializer*/
|
||||
#define OPUS_OK 0
|
||||
/** One or more invalid/out of range arguments @hideinitializer*/
|
||||
#define OPUS_BAD_ARG -1
|
||||
/** Not enough bytes allocated in the buffer @hideinitializer*/
|
||||
#define OPUS_BUFFER_TOO_SMALL -2
|
||||
/** An internal error was detected @hideinitializer*/
|
||||
#define OPUS_INTERNAL_ERROR -3
|
||||
/** The compressed data passed is corrupted @hideinitializer*/
|
||||
#define OPUS_INVALID_PACKET -4
|
||||
/** Invalid/unsupported request number @hideinitializer*/
|
||||
#define OPUS_UNIMPLEMENTED -5
|
||||
/** An encoder or decoder structure is invalid or already freed @hideinitializer*/
|
||||
#define OPUS_INVALID_STATE -6
|
||||
/** Memory allocation has failed @hideinitializer*/
|
||||
#define OPUS_ALLOC_FAIL -7
|
||||
/**@}*/
|
||||
|
||||
/** @cond OPUS_INTERNAL_DOC */
|
||||
/**Export control for opus functions */
|
||||
|
||||
#ifndef OPUS_EXPORT
|
||||
# if defined(WIN32)
|
||||
# if defined(OPUS_BUILD) && defined(DLL_EXPORT)
|
||||
# define OPUS_EXPORT __declspec(dllexport)
|
||||
# else
|
||||
# define OPUS_EXPORT
|
||||
# endif
|
||||
# elif defined(__GNUC__) && defined(OPUS_BUILD)
|
||||
# define OPUS_EXPORT __attribute__ ((visibility ("default")))
|
||||
# else
|
||||
# define OPUS_EXPORT
|
||||
# endif
|
||||
#endif
|
||||
|
||||
# if !defined(OPUS_GNUC_PREREQ)
|
||||
# if defined(__GNUC__)&&defined(__GNUC_MINOR__)
|
||||
# define OPUS_GNUC_PREREQ(_maj,_min) \
|
||||
((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min))
|
||||
# else
|
||||
# define OPUS_GNUC_PREREQ(_maj,_min) 0
|
||||
# endif
|
||||
# endif
|
||||
|
||||
#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) )
|
||||
# if OPUS_GNUC_PREREQ(3,0)
|
||||
# define OPUS_RESTRICT __restrict__
|
||||
# elif (defined(_MSC_VER) && _MSC_VER >= 1400)
|
||||
# define OPUS_RESTRICT __restrict
|
||||
# else
|
||||
# define OPUS_RESTRICT
|
||||
# endif
|
||||
#else
|
||||
# define OPUS_RESTRICT restrict
|
||||
#endif
|
||||
|
||||
#if (!defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) )
|
||||
# if OPUS_GNUC_PREREQ(2,7)
|
||||
# define OPUS_INLINE __inline__
|
||||
# elif (defined(_MSC_VER))
|
||||
# define OPUS_INLINE __inline
|
||||
# else
|
||||
# define OPUS_INLINE
|
||||
# endif
|
||||
#else
|
||||
# define OPUS_INLINE inline
|
||||
#endif
|
||||
|
||||
/**Warning attributes for opus functions
|
||||
* NONNULL is not used in OPUS_BUILD to avoid the compiler optimizing out
|
||||
* some paranoid null checks. */
|
||||
#if defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4)
|
||||
# define OPUS_WARN_UNUSED_RESULT __attribute__ ((__warn_unused_result__))
|
||||
#else
|
||||
# define OPUS_WARN_UNUSED_RESULT
|
||||
#endif
|
||||
#if !defined(OPUS_BUILD) && defined(__GNUC__) && OPUS_GNUC_PREREQ(3, 4)
|
||||
# define OPUS_ARG_NONNULL(_x) __attribute__ ((__nonnull__(_x)))
|
||||
#else
|
||||
# define OPUS_ARG_NONNULL(_x)
|
||||
#endif
|
||||
|
||||
/** These are the actual Encoder CTL ID numbers.
|
||||
* They should not be used directly by applications.
|
||||
* In general, SETs should be even and GETs should be odd.*/
|
||||
#define OPUS_SET_APPLICATION_REQUEST 4000
|
||||
#define OPUS_GET_APPLICATION_REQUEST 4001
|
||||
#define OPUS_SET_BITRATE_REQUEST 4002
|
||||
#define OPUS_GET_BITRATE_REQUEST 4003
|
||||
#define OPUS_SET_MAX_BANDWIDTH_REQUEST 4004
|
||||
#define OPUS_GET_MAX_BANDWIDTH_REQUEST 4005
|
||||
#define OPUS_SET_VBR_REQUEST 4006
|
||||
#define OPUS_GET_VBR_REQUEST 4007
|
||||
#define OPUS_SET_BANDWIDTH_REQUEST 4008
|
||||
#define OPUS_GET_BANDWIDTH_REQUEST 4009
|
||||
#define OPUS_SET_COMPLEXITY_REQUEST 4010
|
||||
#define OPUS_GET_COMPLEXITY_REQUEST 4011
|
||||
#define OPUS_SET_INBAND_FEC_REQUEST 4012
|
||||
#define OPUS_GET_INBAND_FEC_REQUEST 4013
|
||||
#define OPUS_SET_PACKET_LOSS_PERC_REQUEST 4014
|
||||
#define OPUS_GET_PACKET_LOSS_PERC_REQUEST 4015
|
||||
#define OPUS_SET_DTX_REQUEST 4016
|
||||
#define OPUS_GET_DTX_REQUEST 4017
|
||||
#define OPUS_SET_VBR_CONSTRAINT_REQUEST 4020
|
||||
#define OPUS_GET_VBR_CONSTRAINT_REQUEST 4021
|
||||
#define OPUS_SET_FORCE_CHANNELS_REQUEST 4022
|
||||
#define OPUS_GET_FORCE_CHANNELS_REQUEST 4023
|
||||
#define OPUS_SET_SIGNAL_REQUEST 4024
|
||||
#define OPUS_GET_SIGNAL_REQUEST 4025
|
||||
#define OPUS_GET_LOOKAHEAD_REQUEST 4027
|
||||
/* #define OPUS_RESET_STATE 4028 */
|
||||
#define OPUS_GET_SAMPLE_RATE_REQUEST 4029
|
||||
#define OPUS_GET_FINAL_RANGE_REQUEST 4031
|
||||
#define OPUS_GET_PITCH_REQUEST 4033
|
||||
#define OPUS_SET_GAIN_REQUEST 4034
|
||||
#define OPUS_GET_GAIN_REQUEST 4045 /* Should have been 4035 */
|
||||
#define OPUS_SET_LSB_DEPTH_REQUEST 4036
|
||||
#define OPUS_GET_LSB_DEPTH_REQUEST 4037
|
||||
#define OPUS_GET_LAST_PACKET_DURATION_REQUEST 4039
|
||||
#define OPUS_SET_EXPERT_FRAME_DURATION_REQUEST 4040
|
||||
#define OPUS_GET_EXPERT_FRAME_DURATION_REQUEST 4041
|
||||
#define OPUS_SET_PREDICTION_DISABLED_REQUEST 4042
|
||||
#define OPUS_GET_PREDICTION_DISABLED_REQUEST 4043
|
||||
/* Don't use 4045, it's already taken by OPUS_GET_GAIN_REQUEST */
|
||||
#define OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST 4046
|
||||
#define OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST 4047
|
||||
|
||||
/** Defines for the presence of extended APIs. */
|
||||
#define OPUS_HAVE_OPUS_PROJECTION_H
|
||||
|
||||
/* Macros to trigger compilation errors when the wrong types are provided to a CTL */
|
||||
#define __opus_check_int(x) (((void)((x) == (opus_int32)0)), (opus_int32)(x))
|
||||
#define __opus_check_int_ptr(ptr) ((ptr) + ((ptr) - (opus_int32*)(ptr)))
|
||||
#define __opus_check_uint_ptr(ptr) ((ptr) + ((ptr) - (opus_uint32*)(ptr)))
|
||||
#define __opus_check_val16_ptr(ptr) ((ptr) + ((ptr) - (opus_val16*)(ptr)))
|
||||
/** @endcond */
|
||||
|
||||
/** @defgroup opus_ctlvalues Pre-defined values for CTL interface
|
||||
* @see opus_genericctls, opus_encoderctls
|
||||
* @{
|
||||
*/
|
||||
/* Values for the various encoder CTLs */
|
||||
#define OPUS_AUTO -1000 /**<Auto/default setting @hideinitializer*/
|
||||
#define OPUS_BITRATE_MAX -1 /**<Maximum bitrate @hideinitializer*/
|
||||
|
||||
/** Best for most VoIP/videoconference applications where listening quality and intelligibility matter most
|
||||
* @hideinitializer */
|
||||
#define OPUS_APPLICATION_VOIP 2048
|
||||
/** Best for broadcast/high-fidelity application where the decoded audio should be as close as possible to the input
|
||||
* @hideinitializer */
|
||||
#define OPUS_APPLICATION_AUDIO 2049
|
||||
/** Only use when lowest-achievable latency is what matters most. Voice-optimized modes cannot be used.
|
||||
* @hideinitializer */
|
||||
#define OPUS_APPLICATION_RESTRICTED_LOWDELAY 2051
|
||||
|
||||
#define OPUS_SIGNAL_VOICE 3001 /**< Signal being encoded is voice */
|
||||
#define OPUS_SIGNAL_MUSIC 3002 /**< Signal being encoded is music */
|
||||
#define OPUS_BANDWIDTH_NARROWBAND 1101 /**< 4 kHz bandpass @hideinitializer*/
|
||||
#define OPUS_BANDWIDTH_MEDIUMBAND 1102 /**< 6 kHz bandpass @hideinitializer*/
|
||||
#define OPUS_BANDWIDTH_WIDEBAND 1103 /**< 8 kHz bandpass @hideinitializer*/
|
||||
#define OPUS_BANDWIDTH_SUPERWIDEBAND 1104 /**<12 kHz bandpass @hideinitializer*/
|
||||
#define OPUS_BANDWIDTH_FULLBAND 1105 /**<20 kHz bandpass @hideinitializer*/
|
||||
|
||||
#define OPUS_FRAMESIZE_ARG 5000 /**< Select frame size from the argument (default) */
|
||||
#define OPUS_FRAMESIZE_2_5_MS 5001 /**< Use 2.5 ms frames */
|
||||
#define OPUS_FRAMESIZE_5_MS 5002 /**< Use 5 ms frames */
|
||||
#define OPUS_FRAMESIZE_10_MS 5003 /**< Use 10 ms frames */
|
||||
#define OPUS_FRAMESIZE_20_MS 5004 /**< Use 20 ms frames */
|
||||
#define OPUS_FRAMESIZE_40_MS 5005 /**< Use 40 ms frames */
|
||||
#define OPUS_FRAMESIZE_60_MS 5006 /**< Use 60 ms frames */
|
||||
#define OPUS_FRAMESIZE_80_MS 5007 /**< Use 80 ms frames */
|
||||
#define OPUS_FRAMESIZE_100_MS 5008 /**< Use 100 ms frames */
|
||||
#define OPUS_FRAMESIZE_120_MS 5009 /**< Use 120 ms frames */
|
||||
|
||||
/**@}*/
|
||||
|
||||
|
||||
/** @defgroup opus_encoderctls Encoder related CTLs
|
||||
*
|
||||
* These are convenience macros for use with the \c opus_encode_ctl
|
||||
* interface. They are used to generate the appropriate series of
|
||||
* arguments for that call, passing the correct type, size and so
|
||||
* on as expected for each particular request.
|
||||
*
|
||||
* Some usage examples:
|
||||
*
|
||||
* @code
|
||||
* int ret;
|
||||
* ret = opus_encoder_ctl(enc_ctx, OPUS_SET_BANDWIDTH(OPUS_AUTO));
|
||||
* if (ret != OPUS_OK) return ret;
|
||||
*
|
||||
* opus_int32 rate;
|
||||
* opus_encoder_ctl(enc_ctx, OPUS_GET_BANDWIDTH(&rate));
|
||||
*
|
||||
* opus_encoder_ctl(enc_ctx, OPUS_RESET_STATE);
|
||||
* @endcode
|
||||
*
|
||||
* @see opus_genericctls, opus_encoder
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** Configures the encoder's computational complexity.
|
||||
* The supported range is 0-10 inclusive with 10 representing the highest complexity.
|
||||
* @see OPUS_GET_COMPLEXITY
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values: 0-10, inclusive.
|
||||
*
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_COMPLEXITY(x) OPUS_SET_COMPLEXITY_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's complexity configuration.
|
||||
* @see OPUS_SET_COMPLEXITY
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns a value in the range 0-10,
|
||||
* inclusive.
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_COMPLEXITY(x) OPUS_GET_COMPLEXITY_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures the bitrate in the encoder.
|
||||
* Rates from 500 to 512000 bits per second are meaningful, as well as the
|
||||
* special values #OPUS_AUTO and #OPUS_BITRATE_MAX.
|
||||
* The value #OPUS_BITRATE_MAX can be used to cause the codec to use as much
|
||||
* rate as it can, which is useful for controlling the rate by adjusting the
|
||||
* output buffer size.
|
||||
* @see OPUS_GET_BITRATE
|
||||
* @param[in] x <tt>opus_int32</tt>: Bitrate in bits per second. The default
|
||||
* is determined based on the number of
|
||||
* channels and the input sampling rate.
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_BITRATE(x) OPUS_SET_BITRATE_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's bitrate configuration.
|
||||
* @see OPUS_SET_BITRATE
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns the bitrate in bits per second.
|
||||
* The default is determined based on the
|
||||
* number of channels and the input
|
||||
* sampling rate.
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_BITRATE(x) OPUS_GET_BITRATE_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Enables or disables variable bitrate (VBR) in the encoder.
|
||||
* The configured bitrate may not be met exactly because frames must
|
||||
* be an integer number of bytes in length.
|
||||
* @see OPUS_GET_VBR
|
||||
* @see OPUS_SET_VBR_CONSTRAINT
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Hard CBR. For LPC/hybrid modes at very low bit-rate, this can
|
||||
* cause noticeable quality degradation.</dd>
|
||||
* <dt>1</dt><dd>VBR (default). The exact type of VBR is controlled by
|
||||
* #OPUS_SET_VBR_CONSTRAINT.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_VBR(x) OPUS_SET_VBR_REQUEST, __opus_check_int(x)
|
||||
/** Determine if variable bitrate (VBR) is enabled in the encoder.
|
||||
* @see OPUS_SET_VBR
|
||||
* @see OPUS_GET_VBR_CONSTRAINT
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Hard CBR.</dd>
|
||||
* <dt>1</dt><dd>VBR (default). The exact type of VBR may be retrieved via
|
||||
* #OPUS_GET_VBR_CONSTRAINT.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_VBR(x) OPUS_GET_VBR_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Enables or disables constrained VBR in the encoder.
|
||||
* This setting is ignored when the encoder is in CBR mode.
|
||||
* @warning Only the MDCT mode of Opus currently heeds the constraint.
|
||||
* Speech mode ignores it completely, hybrid mode may fail to obey it
|
||||
* if the LPC layer uses more bitrate than the constraint would have
|
||||
* permitted.
|
||||
* @see OPUS_GET_VBR_CONSTRAINT
|
||||
* @see OPUS_SET_VBR
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Unconstrained VBR.</dd>
|
||||
* <dt>1</dt><dd>Constrained VBR (default). This creates a maximum of one
|
||||
* frame of buffering delay assuming a transport with a
|
||||
* serialization speed of the nominal bitrate.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_VBR_CONSTRAINT(x) OPUS_SET_VBR_CONSTRAINT_REQUEST, __opus_check_int(x)
|
||||
/** Determine if constrained VBR is enabled in the encoder.
|
||||
* @see OPUS_SET_VBR_CONSTRAINT
|
||||
* @see OPUS_GET_VBR
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Unconstrained VBR.</dd>
|
||||
* <dt>1</dt><dd>Constrained VBR (default).</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_VBR_CONSTRAINT(x) OPUS_GET_VBR_CONSTRAINT_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures mono/stereo forcing in the encoder.
|
||||
* This can force the encoder to produce packets encoded as either mono or
|
||||
* stereo, regardless of the format of the input audio. This is useful when
|
||||
* the caller knows that the input signal is currently a mono source embedded
|
||||
* in a stereo stream.
|
||||
* @see OPUS_GET_FORCE_CHANNELS
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_AUTO</dt><dd>Not forced (default)</dd>
|
||||
* <dt>1</dt> <dd>Forced mono</dd>
|
||||
* <dt>2</dt> <dd>Forced stereo</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_FORCE_CHANNELS(x) OPUS_SET_FORCE_CHANNELS_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's forced channel configuration.
|
||||
* @see OPUS_SET_FORCE_CHANNELS
|
||||
* @param[out] x <tt>opus_int32 *</tt>:
|
||||
* <dl>
|
||||
* <dt>#OPUS_AUTO</dt><dd>Not forced (default)</dd>
|
||||
* <dt>1</dt> <dd>Forced mono</dd>
|
||||
* <dt>2</dt> <dd>Forced stereo</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_FORCE_CHANNELS(x) OPUS_GET_FORCE_CHANNELS_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures the maximum bandpass that the encoder will select automatically.
|
||||
* Applications should normally use this instead of #OPUS_SET_BANDWIDTH
|
||||
* (leaving that set to the default, #OPUS_AUTO). This allows the
|
||||
* application to set an upper bound based on the type of input it is
|
||||
* providing, but still gives the encoder the freedom to reduce the bandpass
|
||||
* when the bitrate becomes too low, for better overall quality.
|
||||
* @see OPUS_GET_MAX_BANDWIDTH
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>OPUS_BANDWIDTH_NARROWBAND</dt> <dd>4 kHz passband</dd>
|
||||
* <dt>OPUS_BANDWIDTH_MEDIUMBAND</dt> <dd>6 kHz passband</dd>
|
||||
* <dt>OPUS_BANDWIDTH_WIDEBAND</dt> <dd>8 kHz passband</dd>
|
||||
* <dt>OPUS_BANDWIDTH_SUPERWIDEBAND</dt><dd>12 kHz passband</dd>
|
||||
* <dt>OPUS_BANDWIDTH_FULLBAND</dt> <dd>20 kHz passband (default)</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_MAX_BANDWIDTH(x) OPUS_SET_MAX_BANDWIDTH_REQUEST, __opus_check_int(x)
|
||||
|
||||
/** Gets the encoder's configured maximum allowed bandpass.
|
||||
* @see OPUS_SET_MAX_BANDWIDTH
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_BANDWIDTH_NARROWBAND</dt> <dd>4 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_MEDIUMBAND</dt> <dd>6 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_WIDEBAND</dt> <dd>8 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_SUPERWIDEBAND</dt><dd>12 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_FULLBAND</dt> <dd>20 kHz passband (default)</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_MAX_BANDWIDTH(x) OPUS_GET_MAX_BANDWIDTH_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Sets the encoder's bandpass to a specific value.
|
||||
* This prevents the encoder from automatically selecting the bandpass based
|
||||
* on the available bitrate. If an application knows the bandpass of the input
|
||||
* audio it is providing, it should normally use #OPUS_SET_MAX_BANDWIDTH
|
||||
* instead, which still gives the encoder the freedom to reduce the bandpass
|
||||
* when the bitrate becomes too low, for better overall quality.
|
||||
* @see OPUS_GET_BANDWIDTH
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_AUTO</dt> <dd>(default)</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_NARROWBAND</dt> <dd>4 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_MEDIUMBAND</dt> <dd>6 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_WIDEBAND</dt> <dd>8 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_SUPERWIDEBAND</dt><dd>12 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_FULLBAND</dt> <dd>20 kHz passband</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_BANDWIDTH(x) OPUS_SET_BANDWIDTH_REQUEST, __opus_check_int(x)
|
||||
|
||||
/** Configures the type of signal being encoded.
|
||||
* This is a hint which helps the encoder's mode selection.
|
||||
* @see OPUS_GET_SIGNAL
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_AUTO</dt> <dd>(default)</dd>
|
||||
* <dt>#OPUS_SIGNAL_VOICE</dt><dd>Bias thresholds towards choosing LPC or Hybrid modes.</dd>
|
||||
* <dt>#OPUS_SIGNAL_MUSIC</dt><dd>Bias thresholds towards choosing MDCT modes.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_SIGNAL(x) OPUS_SET_SIGNAL_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured signal type.
|
||||
* @see OPUS_SET_SIGNAL
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_AUTO</dt> <dd>(default)</dd>
|
||||
* <dt>#OPUS_SIGNAL_VOICE</dt><dd>Bias thresholds towards choosing LPC or Hybrid modes.</dd>
|
||||
* <dt>#OPUS_SIGNAL_MUSIC</dt><dd>Bias thresholds towards choosing MDCT modes.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_SIGNAL(x) OPUS_GET_SIGNAL_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
|
||||
/** Configures the encoder's intended application.
|
||||
* The initial value is a mandatory argument to the encoder_create function.
|
||||
* @see OPUS_GET_APPLICATION
|
||||
* @param[in] x <tt>opus_int32</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_APPLICATION_VOIP</dt>
|
||||
* <dd>Process signal for improved speech intelligibility.</dd>
|
||||
* <dt>#OPUS_APPLICATION_AUDIO</dt>
|
||||
* <dd>Favor faithfulness to the original input.</dd>
|
||||
* <dt>#OPUS_APPLICATION_RESTRICTED_LOWDELAY</dt>
|
||||
* <dd>Configure the minimum possible coding delay by disabling certain modes
|
||||
* of operation.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_APPLICATION(x) OPUS_SET_APPLICATION_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured application.
|
||||
* @see OPUS_SET_APPLICATION
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_APPLICATION_VOIP</dt>
|
||||
* <dd>Process signal for improved speech intelligibility.</dd>
|
||||
* <dt>#OPUS_APPLICATION_AUDIO</dt>
|
||||
* <dd>Favor faithfulness to the original input.</dd>
|
||||
* <dt>#OPUS_APPLICATION_RESTRICTED_LOWDELAY</dt>
|
||||
* <dd>Configure the minimum possible coding delay by disabling certain modes
|
||||
* of operation.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_APPLICATION(x) OPUS_GET_APPLICATION_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Gets the total samples of delay added by the entire codec.
|
||||
* This can be queried by the encoder and then the provided number of samples can be
|
||||
* skipped on from the start of the decoder's output to provide time aligned input
|
||||
* and output. From the perspective of a decoding application the real data begins this many
|
||||
* samples late.
|
||||
*
|
||||
* The decoder contribution to this delay is identical for all decoders, but the
|
||||
* encoder portion of the delay may vary from implementation to implementation,
|
||||
* version to version, or even depend on the encoder's initial configuration.
|
||||
* Applications needing delay compensation should call this CTL rather than
|
||||
* hard-coding a value.
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Number of lookahead samples
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_LOOKAHEAD(x) OPUS_GET_LOOKAHEAD_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures the encoder's use of inband forward error correction (FEC).
|
||||
* @note This is only applicable to the LPC layer
|
||||
* @see OPUS_GET_INBAND_FEC
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Disable inband FEC (default).</dd>
|
||||
* <dt>1</dt><dd>Enable inband FEC.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_INBAND_FEC(x) OPUS_SET_INBAND_FEC_REQUEST, __opus_check_int(x)
|
||||
/** Gets encoder's configured use of inband forward error correction.
|
||||
* @see OPUS_SET_INBAND_FEC
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Inband FEC disabled (default).</dd>
|
||||
* <dt>1</dt><dd>Inband FEC enabled.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_INBAND_FEC(x) OPUS_GET_INBAND_FEC_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures the encoder's expected packet loss percentage.
|
||||
* Higher values trigger progressively more loss resistant behavior in the encoder
|
||||
* at the expense of quality at a given bitrate in the absence of packet loss, but
|
||||
* greater quality under loss.
|
||||
* @see OPUS_GET_PACKET_LOSS_PERC
|
||||
* @param[in] x <tt>opus_int32</tt>: Loss percentage in the range 0-100, inclusive (default: 0).
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_PACKET_LOSS_PERC(x) OPUS_SET_PACKET_LOSS_PERC_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured packet loss percentage.
|
||||
* @see OPUS_SET_PACKET_LOSS_PERC
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns the configured loss percentage
|
||||
* in the range 0-100, inclusive (default: 0).
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_PACKET_LOSS_PERC(x) OPUS_GET_PACKET_LOSS_PERC_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures the encoder's use of discontinuous transmission (DTX).
|
||||
* @note This is only applicable to the LPC layer
|
||||
* @see OPUS_GET_DTX
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Disable DTX (default).</dd>
|
||||
* <dt>1</dt><dd>Enabled DTX.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_DTX(x) OPUS_SET_DTX_REQUEST, __opus_check_int(x)
|
||||
/** Gets encoder's configured use of discontinuous transmission.
|
||||
* @see OPUS_SET_DTX
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>DTX disabled (default).</dd>
|
||||
* <dt>1</dt><dd>DTX enabled.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_DTX(x) OPUS_GET_DTX_REQUEST, __opus_check_int_ptr(x)
|
||||
/** Configures the depth of signal being encoded.
|
||||
*
|
||||
* This is a hint which helps the encoder identify silence and near-silence.
|
||||
* It represents the number of significant bits of linear intensity below
|
||||
* which the signal contains ignorable quantization or other noise.
|
||||
*
|
||||
* For example, OPUS_SET_LSB_DEPTH(14) would be an appropriate setting
|
||||
* for G.711 u-law input. OPUS_SET_LSB_DEPTH(16) would be appropriate
|
||||
* for 16-bit linear pcm input with opus_encode_float().
|
||||
*
|
||||
* When using opus_encode() instead of opus_encode_float(), or when libopus
|
||||
* is compiled for fixed-point, the encoder uses the minimum of the value
|
||||
* set here and the value 16.
|
||||
*
|
||||
* @see OPUS_GET_LSB_DEPTH
|
||||
* @param[in] x <tt>opus_int32</tt>: Input precision in bits, between 8 and 24
|
||||
* (default: 24).
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_LSB_DEPTH(x) OPUS_SET_LSB_DEPTH_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured signal depth.
|
||||
* @see OPUS_SET_LSB_DEPTH
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Input precision in bits, between 8 and
|
||||
* 24 (default: 24).
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_LSB_DEPTH(x) OPUS_GET_LSB_DEPTH_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Configures the encoder's use of variable duration frames.
|
||||
* When variable duration is enabled, the encoder is free to use a shorter frame
|
||||
* size than the one requested in the opus_encode*() call.
|
||||
* It is then the user's responsibility
|
||||
* to verify how much audio was encoded by checking the ToC byte of the encoded
|
||||
* packet. The part of the audio that was not encoded needs to be resent to the
|
||||
* encoder for the next call. Do not use this option unless you <b>really</b>
|
||||
* know what you are doing.
|
||||
* @see OPUS_GET_EXPERT_FRAME_DURATION
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>OPUS_FRAMESIZE_ARG</dt><dd>Select frame size from the argument (default).</dd>
|
||||
* <dt>OPUS_FRAMESIZE_2_5_MS</dt><dd>Use 2.5 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_5_MS</dt><dd>Use 5 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_10_MS</dt><dd>Use 10 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_20_MS</dt><dd>Use 20 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_40_MS</dt><dd>Use 40 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_60_MS</dt><dd>Use 60 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_80_MS</dt><dd>Use 80 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_100_MS</dt><dd>Use 100 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_120_MS</dt><dd>Use 120 ms frames.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_EXPERT_FRAME_DURATION(x) OPUS_SET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured use of variable duration frames.
|
||||
* @see OPUS_SET_EXPERT_FRAME_DURATION
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>OPUS_FRAMESIZE_ARG</dt><dd>Select frame size from the argument (default).</dd>
|
||||
* <dt>OPUS_FRAMESIZE_2_5_MS</dt><dd>Use 2.5 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_5_MS</dt><dd>Use 5 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_10_MS</dt><dd>Use 10 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_20_MS</dt><dd>Use 20 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_40_MS</dt><dd>Use 40 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_60_MS</dt><dd>Use 60 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_80_MS</dt><dd>Use 80 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_100_MS</dt><dd>Use 100 ms frames.</dd>
|
||||
* <dt>OPUS_FRAMESIZE_120_MS</dt><dd>Use 120 ms frames.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_EXPERT_FRAME_DURATION(x) OPUS_GET_EXPERT_FRAME_DURATION_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** If set to 1, disables almost all use of prediction, making frames almost
|
||||
* completely independent. This reduces quality.
|
||||
* @see OPUS_GET_PREDICTION_DISABLED
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Enable prediction (default).</dd>
|
||||
* <dt>1</dt><dd>Disable prediction.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_PREDICTION_DISABLED(x) OPUS_SET_PREDICTION_DISABLED_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured prediction status.
|
||||
* @see OPUS_SET_PREDICTION_DISABLED
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Prediction enabled (default).</dd>
|
||||
* <dt>1</dt><dd>Prediction disabled.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_PREDICTION_DISABLED(x) OPUS_GET_PREDICTION_DISABLED_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/**@}*/
|
||||
|
||||
/** @defgroup opus_genericctls Generic CTLs
|
||||
*
|
||||
* These macros are used with the \c opus_decoder_ctl and
|
||||
* \c opus_encoder_ctl calls to generate a particular
|
||||
* request.
|
||||
*
|
||||
* When called on an \c OpusDecoder they apply to that
|
||||
* particular decoder instance. When called on an
|
||||
* \c OpusEncoder they apply to the corresponding setting
|
||||
* on that encoder instance, if present.
|
||||
*
|
||||
* Some usage examples:
|
||||
*
|
||||
* @code
|
||||
* int ret;
|
||||
* opus_int32 pitch;
|
||||
* ret = opus_decoder_ctl(dec_ctx, OPUS_GET_PITCH(&pitch));
|
||||
* if (ret == OPUS_OK) return ret;
|
||||
*
|
||||
* opus_encoder_ctl(enc_ctx, OPUS_RESET_STATE);
|
||||
* opus_decoder_ctl(dec_ctx, OPUS_RESET_STATE);
|
||||
*
|
||||
* opus_int32 enc_bw, dec_bw;
|
||||
* opus_encoder_ctl(enc_ctx, OPUS_GET_BANDWIDTH(&enc_bw));
|
||||
* opus_decoder_ctl(dec_ctx, OPUS_GET_BANDWIDTH(&dec_bw));
|
||||
* if (enc_bw != dec_bw) {
|
||||
* printf("packet bandwidth mismatch!\n");
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @see opus_encoder, opus_decoder_ctl, opus_encoder_ctl, opus_decoderctls, opus_encoderctls
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** Resets the codec state to be equivalent to a freshly initialized state.
|
||||
* This should be called when switching streams in order to prevent
|
||||
* the back to back decoding from giving different results from
|
||||
* one at a time decoding.
|
||||
* @hideinitializer */
|
||||
#define OPUS_RESET_STATE 4028
|
||||
|
||||
/** Gets the final state of the codec's entropy coder.
|
||||
* This is used for testing purposes,
|
||||
* The encoder and decoder state should be identical after coding a payload
|
||||
* (assuming no data corruption or software bugs)
|
||||
*
|
||||
* @param[out] x <tt>opus_uint32 *</tt>: Entropy coder state
|
||||
*
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_FINAL_RANGE(x) OPUS_GET_FINAL_RANGE_REQUEST, __opus_check_uint_ptr(x)
|
||||
|
||||
/** Gets the encoder's configured bandpass or the decoder's last bandpass.
|
||||
* @see OPUS_SET_BANDWIDTH
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>#OPUS_AUTO</dt> <dd>(default)</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_NARROWBAND</dt> <dd>4 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_MEDIUMBAND</dt> <dd>6 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_WIDEBAND</dt> <dd>8 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_SUPERWIDEBAND</dt><dd>12 kHz passband</dd>
|
||||
* <dt>#OPUS_BANDWIDTH_FULLBAND</dt> <dd>20 kHz passband</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_BANDWIDTH(x) OPUS_GET_BANDWIDTH_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Gets the sampling rate the encoder or decoder was initialized with.
|
||||
* This simply returns the <code>Fs</code> value passed to opus_encoder_init()
|
||||
* or opus_decoder_init().
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Sampling rate of encoder or decoder.
|
||||
* @hideinitializer
|
||||
*/
|
||||
#define OPUS_GET_SAMPLE_RATE(x) OPUS_GET_SAMPLE_RATE_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** If set to 1, disables the use of phase inversion for intensity stereo,
|
||||
* improving the quality of mono downmixes, but slightly reducing normal
|
||||
* stereo quality. Disabling phase inversion in the decoder does not comply
|
||||
* with RFC 6716, although it does not cause any interoperability issue and
|
||||
* is expected to become part of the Opus standard once RFC 6716 is updated
|
||||
* by draft-ietf-codec-opus-update.
|
||||
* @see OPUS_GET_PHASE_INVERSION_DISABLED
|
||||
* @param[in] x <tt>opus_int32</tt>: Allowed values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Enable phase inversion (default).</dd>
|
||||
* <dt>1</dt><dd>Disable phase inversion.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_PHASE_INVERSION_DISABLED(x) OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int(x)
|
||||
/** Gets the encoder's configured phase inversion status.
|
||||
* @see OPUS_SET_PHASE_INVERSION_DISABLED
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Returns one of the following values:
|
||||
* <dl>
|
||||
* <dt>0</dt><dd>Stereo phase inversion enabled (default).</dd>
|
||||
* <dt>1</dt><dd>Stereo phase inversion disabled.</dd>
|
||||
* </dl>
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_PHASE_INVERSION_DISABLED(x) OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/**@}*/
|
||||
|
||||
/** @defgroup opus_decoderctls Decoder related CTLs
|
||||
* @see opus_genericctls, opus_encoderctls, opus_decoder
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** Configures decoder gain adjustment.
|
||||
* Scales the decoded output by a factor specified in Q8 dB units.
|
||||
* This has a maximum range of -32768 to 32767 inclusive, and returns
|
||||
* OPUS_BAD_ARG otherwise. The default is zero indicating no adjustment.
|
||||
* This setting survives decoder reset.
|
||||
*
|
||||
* gain = pow(10, x/(20.0*256))
|
||||
*
|
||||
* @param[in] x <tt>opus_int32</tt>: Amount to scale PCM signal by in Q8 dB units.
|
||||
* @hideinitializer */
|
||||
#define OPUS_SET_GAIN(x) OPUS_SET_GAIN_REQUEST, __opus_check_int(x)
|
||||
/** Gets the decoder's configured gain adjustment. @see OPUS_SET_GAIN
|
||||
*
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Amount to scale PCM signal by in Q8 dB units.
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_GAIN(x) OPUS_GET_GAIN_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Gets the duration (in samples) of the last packet successfully decoded or concealed.
|
||||
* @param[out] x <tt>opus_int32 *</tt>: Number of samples (at current sampling rate).
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_LAST_PACKET_DURATION(x) OPUS_GET_LAST_PACKET_DURATION_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/** Gets the pitch of the last decoded frame, if available.
|
||||
* This can be used for any post-processing algorithm requiring the use of pitch,
|
||||
* e.g. time stretching/shortening. If the last frame was not voiced, or if the
|
||||
* pitch was not coded in the frame, then zero is returned.
|
||||
*
|
||||
* This CTL is only implemented for decoder instances.
|
||||
*
|
||||
* @param[out] x <tt>opus_int32 *</tt>: pitch period at 48 kHz (or 0 if not available)
|
||||
*
|
||||
* @hideinitializer */
|
||||
#define OPUS_GET_PITCH(x) OPUS_GET_PITCH_REQUEST, __opus_check_int_ptr(x)
|
||||
|
||||
/**@}*/
|
||||
|
||||
/** @defgroup opus_libinfo Opus library information functions
|
||||
* @{
|
||||
*/
|
||||
|
||||
/** Converts an opus error code into a human readable string.
|
||||
*
|
||||
* @param[in] error <tt>int</tt>: Error number
|
||||
* @returns Error string
|
||||
*/
|
||||
OPUS_EXPORT const char *opus_strerror(int error);
|
||||
|
||||
/** Gets the libopus version string.
|
||||
*
|
||||
* Applications may look for the substring "-fixed" in the version string to
|
||||
* determine whether they have a fixed-point or floating-point build at
|
||||
* runtime.
|
||||
*
|
||||
* @returns Version string
|
||||
*/
|
||||
OPUS_EXPORT const char *opus_get_version_string(void);
|
||||
/**@}*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OPUS_DEFINES_H */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user