Compare commits
766 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 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 | |||
| 36191781ed | |||
| 61b6a49669 | |||
| e97845e46e | |||
| 6bba68207d | |||
| 0e17cccc06 | |||
| 918e922e40 | |||
| a08854ddfd | |||
| eb6f15c2b7 | |||
| 2cd9e31684 | |||
| 791d6624e2 | |||
| af41021271 | |||
| d726d939f4 | |||
| 748085e7bb | |||
| b64c84a5c3 | |||
| d57d19174b | |||
| efebe1828a | |||
| 06007e0597 | |||
| 42a502eff1 | |||
| 3a868045d7 | |||
| e0a7ff1880 | |||
| 88d43bbd40 | |||
| 30ff319b13 | |||
| 9a0f48b799 | |||
| b52c8a1a8f | |||
| 3fde115670 | |||
| b6f4d8ff1e | |||
| 46d72b912c | |||
| 722f397819 | |||
| a7d85a7dd5 | |||
| 9b238ab6c3 | |||
| f82ee97c05 | |||
| 8f169b976b | |||
| 35fb96f9f4 | |||
| 37371906d5 | |||
| 53452d22c0 | |||
| 9d4e9631bc | |||
| 83a9539f4b | |||
| b214fe5301 | |||
| 57779b4e89 | |||
| 547932f8b2 | |||
| 762fa0fe2f | |||
| 9cedc57df2 | |||
| ba81f8096a | |||
| c4fa654166 | |||
| 8ac440b68b | |||
| 165386b941 | |||
| 3a7398f321 | |||
| ebb1d0dfa2 | |||
| 1ca1ed5d20 | |||
| b416bafb78 | |||
| 3a301b74a6 | |||
| 71d463f063 | |||
| 1fae816223 | |||
| 989d6fc169 | |||
| 381509b3a6 | |||
| d8ae40376e | |||
| 4ea93f5e68 | |||
| cd84c8f30e | |||
| 8d4cdca7c3 | |||
| c0239c36fd | |||
| 9d9f729e42 | |||
| 6c5fe18b6e | |||
| 1994bf6522 | |||
| 31381e5664 | |||
| fac1b1d7e5 | |||
| 40c406051c | |||
| 8bac873e67 | |||
| a170e1efd7 | |||
| 17bffa8d78 | |||
| 289222749b | |||
| 81d84600d4 | |||
| 0b15fd582d | |||
| cbe4a1cde6 | |||
| 5942545b9c | |||
| 89ef16c02e | |||
| 58b6ed8d00 | |||
| 7d01e1a7a4 | |||
| ab769a1606 | |||
| 3ac9abbab1 | |||
| 288efd0726 | |||
| d2d0ed65d6 | |||
| e697ed72db | |||
| b657c746be | |||
| 947f8db2d5 | |||
| 15857efd36 | |||
| 3fd0f20e10 | |||
| a2e64fd7df | |||
| 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 |
@@ -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.
|
||||||
@@ -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
|
||||||
+8
-2
@@ -1,6 +1,9 @@
|
|||||||
#built application files
|
# built application files
|
||||||
*.apk
|
*.apk
|
||||||
*.ap_
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
output.json
|
||||||
|
out/
|
||||||
|
|
||||||
# files for the dex VM
|
# files for the dex VM
|
||||||
*.dex
|
*.dex
|
||||||
@@ -30,8 +33,11 @@ Thumbs.db
|
|||||||
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
app/app.iml
|
*.iml
|
||||||
|
|
||||||
# Compiled JNI libraries folder
|
# Compiled JNI libraries folder
|
||||||
**/jniLibs
|
**/jniLibs
|
||||||
app/.externalNativeBuild/
|
app/.externalNativeBuild/
|
||||||
|
|
||||||
|
# NDK stuff
|
||||||
|
.cxx/
|
||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
[submodule "moonlight-common"]
|
[submodule "app/src/main/jni/moonlight-core/moonlight-common-c"]
|
||||||
path = moonlight-common
|
path = app/src/main/jni/moonlight-core/moonlight-common-c
|
||||||
url = https://github.com/moonlight-stream/moonlight-common.git
|
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
language: android
|
||||||
|
dist: trusty
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
android:
|
||||||
|
components:
|
||||||
|
- tools
|
||||||
|
- platform-tools
|
||||||
|
- build-tools-29.0.1
|
||||||
|
- android-29
|
||||||
|
|
||||||
|
install:
|
||||||
|
- yes | sdkmanager "ndk-bundle"
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
# Moonlight
|
# Moonlight Android
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
||||||
|
|
||||||
|
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -19,7 +19,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for mo
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
* 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)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/com.limelight/), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
+30
-14
@@ -1,18 +1,14 @@
|
|||||||
import com.android.builder.model.ProductFlavor
|
|
||||||
import org.apache.tools.ant.taskdefs.condition.Os
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 29
|
||||||
buildToolsVersion '27.0.3'
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 27
|
targetSdkVersion 29
|
||||||
|
|
||||||
versionName "5.7"
|
versionName "8.2"
|
||||||
versionCode = 148
|
versionCode = 199
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
flavorDimensions "root"
|
||||||
@@ -47,11 +43,30 @@ android {
|
|||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
|
lintConfig file("lint.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle {
|
||||||
|
language {
|
||||||
|
// Avoid splitting by language, since we allow users
|
||||||
|
// to manually switch language in settings.
|
||||||
|
enableSplit = false
|
||||||
|
}
|
||||||
|
density {
|
||||||
|
// FIXME: This should not be neccessary but we get
|
||||||
|
// weird crashes due to missing drawable resources
|
||||||
|
// when this split is enabled.
|
||||||
|
enableSplit = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
|
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
// To whomever is releasing/using an APK in release mode with
|
// To whomever is releasing/using an APK in release mode with
|
||||||
@@ -86,8 +101,8 @@ android {
|
|||||||
// TL;DR: Leave the following line alone!
|
// TL;DR: Leave the following line alone!
|
||||||
applicationIdSuffix ".unofficial"
|
applicationIdSuffix ".unofficial"
|
||||||
|
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +114,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
|
||||||
implementation 'org.jcodec:jcodec:0.2.3'
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.12.3'
|
||||||
implementation project(':moonlight-common')
|
implementation 'com.squareup.okio:okio:1.17.4'
|
||||||
|
implementation 'org.jmdns:jmdns:3.5.5'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<lint>
|
||||||
|
<issue id="InvalidPackage">
|
||||||
|
<ignore path="**/bcpkix-jdk15on-*.jar"/>
|
||||||
|
</issue>
|
||||||
|
</lint>
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Don't obfuscate code
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
|
# Our code
|
||||||
|
-keep class com.limelight.binding.input.evdev.* {*;}
|
||||||
|
|
||||||
|
# Moonlight common
|
||||||
|
-keep class com.limelight.nvstream.jni.* {*;}
|
||||||
|
|
||||||
|
# Okio
|
||||||
|
-keep class sun.misc.Unsafe {*;}
|
||||||
|
-dontwarn java.nio.file.*
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
# BouncyCastle
|
||||||
|
-keep class org.bouncycastle.jcajce.provider.asymmetric.* {*;}
|
||||||
|
-keep class org.bouncycastle.jcajce.provider.asymmetric.util.* {*;}
|
||||||
|
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.* {*;}
|
||||||
|
-keep class org.bouncycastle.jcajce.provider.digest.** {*;}
|
||||||
|
-keep class org.bouncycastle.jcajce.provider.symmetric.** {*;}
|
||||||
|
-keep class org.bouncycastle.jcajce.spec.* {*;}
|
||||||
|
-keep class org.bouncycastle.jce.** {*;}
|
||||||
|
-dontwarn javax.naming.**
|
||||||
|
|
||||||
|
# jMDNS
|
||||||
|
-dontwarn javax.jmdns.impl.DNSCache
|
||||||
|
-dontwarn org.slf4j.**
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_label" translatable="false">Moonlight (Debug)</string>
|
||||||
|
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-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
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
@@ -32,13 +35,20 @@
|
|||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/atv_banner"
|
android:banner="@drawable/atv_banner"
|
||||||
android:appCategory="game"
|
android:appCategory="game"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
|
android:installLocation="auto"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<provider
|
||||||
|
android:name=".PosterContentProvider"
|
||||||
|
android:authorities="poster.${applicationId}"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
</provider>
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
<uses-library
|
<uses-library
|
||||||
android:name="com.sec.android.app.multiwindow"
|
android:name="com.sec.android.app.multiwindow"
|
||||||
@@ -66,8 +76,9 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppViewShortcutTrampoline"
|
android:name=".ShortcutTrampoline"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -93,15 +104,17 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".preferences.AddComputerManually"
|
android:name=".preferences.AddComputerManually"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
android:windowSoftInputMode="stateVisible"
|
||||||
android:label="Add Computer Manually">
|
android:label="Add Computer Manually">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".Game"
|
android:name=".Game"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -26,6 +26,9 @@ import android.app.Service;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -36,10 +39,13 @@ import android.view.ContextMenu.ContextMenuInfo;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private AppGridAdapter appGridAdapter;
|
private AppGridAdapter appGridAdapter;
|
||||||
private String uuidString;
|
private String uuidString;
|
||||||
@@ -56,10 +62,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private final static int START_OR_RESUME_ID = 1;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int QUIT_ID = 2;
|
||||||
private final static int CANCEL_ID = 3;
|
private final static int CANCEL_ID = 3;
|
||||||
private final static int START_WTIH_QUIT = 4;
|
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 NAME_EXTRA = "Name";
|
||||||
public final static String UUID_EXTRA = "UUID";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
|
public final static String NEW_PAIR_EXTRA = "NewPair";
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
@@ -74,27 +83,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Wait for the binder to be ready
|
// Wait for the binder to be ready
|
||||||
localBinder.waitForReady();
|
localBinder.waitForReady();
|
||||||
|
|
||||||
// Now make the binder visible
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// Get the computer object
|
// Get the computer object
|
||||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
computer = localBinder.getComputer(uuidString);
|
||||||
if (computer == null) {
|
if (computer == null) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
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 {
|
try {
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||||
computer, managerBinder.getUniqueId());
|
computer, localBinder.getUniqueId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
finish();
|
finish();
|
||||||
return;
|
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).
|
// Load the app grid with cached data (if possible).
|
||||||
// This must be done _before_ startComputerUpdates()
|
// This must be done _before_ startComputerUpdates()
|
||||||
// so the initial serverinfo response can update the running
|
// so the initial serverinfo response can update the running
|
||||||
@@ -147,7 +162,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't care about other computers
|
// Don't care about other computers
|
||||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +186,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Disable shortcuts referencing this PC for now
|
// Disable shortcuts referencing this PC for now
|
||||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
shortcutHelper.disableComputerShortcut(details,
|
||||||
getResources().getString(R.string.scut_not_paired));
|
getResources().getString(R.string.scut_not_paired));
|
||||||
|
|
||||||
// Display a toast to the user and quit the activity
|
// Display a toast to the user and quit the activity
|
||||||
@@ -207,7 +222,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
blockingLoadSpinner.dismiss();
|
blockingLoadSpinner.dismiss();
|
||||||
blockingLoadSpinner = null;
|
blockingLoadSpinner = null;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,14 +268,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
|
||||||
TextView label = findViewById(R.id.appListText);
|
TextView label = findViewById(R.id.appListText);
|
||||||
setTitle(labelText);
|
setTitle(computerName);
|
||||||
label.setText(labelText);
|
label.setText(computerName);
|
||||||
|
|
||||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
|
||||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
|
||||||
shortcutHelper.reportShortcutUsed(uuidString);
|
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
@@ -272,7 +284,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||||
updateUiWithAppList(applist);
|
updateUiWithAppList(applist);
|
||||||
LimeLog.info("Loaded applist from cache");
|
LimeLog.info("Loaded applist from cache");
|
||||||
} catch (Exception e) {
|
} catch (IOException | XmlPullParserException e) {
|
||||||
if (lastRawApplist != null) {
|
if (lastRawApplist != null) {
|
||||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -331,10 +343,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
}
|
}
|
||||||
else {
|
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, 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
|
@Override
|
||||||
@@ -346,7 +373,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case START_WTIH_QUIT:
|
case START_WITH_QUIT:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -367,8 +394,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
suspendGridUpdates = true;
|
suspendGridUpdates = true;
|
||||||
ServerHelper.doQuit(AppView.this,
|
ServerHelper.doQuit(AppView.this, computer,
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
app.app, managerBinder, new Runnable() {
|
app.app, managerBinder, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -386,6 +412,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
case CANCEL_ID:
|
case CANCEL_ID:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case VIEW_DETAILS_ID:
|
||||||
|
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
||||||
|
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), 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:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -477,6 +516,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
// This app was removed in the latest app list
|
// This app was removed in the latest app list
|
||||||
if (!foundExistingApp) {
|
if (!foundExistingApp) {
|
||||||
|
shortcutHelper.disableAppShortcut(computer, existingApp.app, "App removed from PC");
|
||||||
appGridAdapter.removeApp(existingApp);
|
appGridAdapter.removeApp(existingApp);
|
||||||
updated = true;
|
updated = true;
|
||||||
|
|
||||||
@@ -520,6 +560,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
UiHelper.applyStatusBarPadding(listView);
|
||||||
registerForContextMenu(listView);
|
registerForContextMenu(listView);
|
||||||
listView.requestFocus();
|
listView.requestFocus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +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 (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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,9 +13,11 @@ import com.limelight.binding.input.virtual_controller.VirtualController;
|
|||||||
import com.limelight.binding.video.CrashListener;
|
import com.limelight.binding.video.CrashListener;
|
||||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
|
import com.limelight.binding.video.PerfOverlayListener;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.input.KeyboardPacket;
|
import com.limelight.nvstream.input.KeyboardPacket;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
@@ -39,6 +41,8 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
@@ -64,12 +68,20 @@ import android.view.Window;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||||
OnSystemUiVisibilityChangeListener, GameGestures
|
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
|
||||||
|
PerfOverlayListener
|
||||||
{
|
{
|
||||||
private int lastMouseX = Integer.MIN_VALUE;
|
private int lastMouseX = Integer.MIN_VALUE;
|
||||||
private int lastMouseY = Integer.MIN_VALUE;
|
private int lastMouseY = Integer.MIN_VALUE;
|
||||||
@@ -95,6 +107,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean displayedFailureDialog = false;
|
private boolean displayedFailureDialog = false;
|
||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
|
private boolean surfaceCreated = false;
|
||||||
|
private boolean attemptedConnection = false;
|
||||||
|
|
||||||
private InputCaptureProvider inputCaptureProvider;
|
private InputCaptureProvider inputCaptureProvider;
|
||||||
private int modifierFlags = 0;
|
private int modifierFlags = 0;
|
||||||
@@ -102,12 +116,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private boolean grabComboDown = false;
|
private boolean grabComboDown = false;
|
||||||
private StreamView streamView;
|
private StreamView streamView;
|
||||||
|
|
||||||
|
private boolean isHidingOverlays;
|
||||||
|
private TextView notificationOverlayView;
|
||||||
|
private int requestedNotificationOverlayVisibility = View.GONE;
|
||||||
|
private TextView performanceOverlayView;
|
||||||
|
|
||||||
private ShortcutHelper shortcutHelper;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
|
||||||
private MediaCodecDecoderRenderer decoderRenderer;
|
private MediaCodecDecoderRenderer decoderRenderer;
|
||||||
private boolean reportedCrash;
|
private boolean reportedCrash;
|
||||||
|
|
||||||
private WifiManager.WifiLock wifiLock;
|
private WifiManager.WifiLock highPerfWifiLock;
|
||||||
|
private WifiManager.WifiLock lowLatencyWifiLock;
|
||||||
|
|
||||||
private boolean connectedToUsbDriverService = false;
|
private boolean connectedToUsbDriverService = false;
|
||||||
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
|
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
|
||||||
@@ -128,10 +148,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
public static final String EXTRA_APP_NAME = "AppName";
|
public static final String EXTRA_APP_NAME = "AppName";
|
||||||
public static final String EXTRA_APP_ID = "AppId";
|
public static final String EXTRA_APP_ID = "AppId";
|
||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
|
||||||
public static final String EXTRA_PC_UUID = "UUID";
|
public static final String EXTRA_PC_UUID = "UUID";
|
||||||
public static final String EXTRA_PC_NAME = "PcName";
|
public static final String EXTRA_PC_NAME = "PcName";
|
||||||
public static final String EXTRA_APP_HDR = "HDR";
|
public static final String EXTRA_APP_HDR = "HDR";
|
||||||
|
public static final String EXTRA_SERVER_CERT = "ServerCert";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -142,10 +162,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// We don't want a title bar
|
// We don't want a title bar
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
// Full-screen and don't let the display go off
|
// Full-screen
|
||||||
getWindow().addFlags(
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
|
||||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
|
|
||||||
// If we're going to use immersive mode, we want to have
|
// If we're going to use immersive mode, we want to have
|
||||||
// the entire screen
|
// the entire screen
|
||||||
@@ -158,6 +176,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We specified userLandscape in the manifest which isn't supported until 4.3,
|
||||||
|
// so we must fall back at runtime to sensorLandscape.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for UI visibility events
|
// Listen for UI visibility events
|
||||||
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
|
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
|
||||||
|
|
||||||
@@ -175,11 +199,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
|
||||||
|
// Allow the activity to layout under notches if the fill-screen option
|
||||||
|
// was turned on by the user
|
||||||
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for events on the game surface
|
// Listen for events on the game surface
|
||||||
streamView = findViewById(R.id.surfaceView);
|
streamView = findViewById(R.id.surfaceView);
|
||||||
streamView.setOnGenericMotionListener(this);
|
streamView.setOnGenericMotionListener(this);
|
||||||
streamView.setOnTouchListener(this);
|
streamView.setOnTouchListener(this);
|
||||||
|
streamView.setInputCallbacks(this);
|
||||||
|
|
||||||
|
notificationOverlayView = findViewById(R.id.notificationOverlay);
|
||||||
|
|
||||||
|
performanceOverlayView = findViewById(R.id.performanceOverlay);
|
||||||
|
|
||||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||||
|
|
||||||
@@ -203,28 +238,50 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// Make sure Wi-Fi is fully powered up
|
// Make sure Wi-Fi is fully powered up
|
||||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
|
highPerfWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Moonlight High Perf Lock");
|
||||||
wifiLock.setReferenceCounted(false);
|
highPerfWifiLock.setReferenceCounted(false);
|
||||||
wifiLock.acquire();
|
highPerfWifiLock.acquire();
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
lowLatencyWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Moonlight Low Latency Lock");
|
||||||
|
lowLatencyWifiLock.setReferenceCounted(false);
|
||||||
|
lowLatencyWifiLock.acquire();
|
||||||
|
}
|
||||||
|
|
||||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
|
||||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||||
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||||
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
|
||||||
|
byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT);
|
||||||
|
|
||||||
|
X509Certificate serverCert = null;
|
||||||
|
try {
|
||||||
|
if (derCertData != null) {
|
||||||
|
serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||||
|
}
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
// Report this shortcut being used
|
||||||
|
ComputerDetails computer = new ComputerDetails();
|
||||||
|
computer.name = pcName;
|
||||||
|
computer.uuid = uuid;
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
shortcutHelper.reportComputerShortcutUsed(computer);
|
||||||
shortcutHelper.reportShortcutUsed(uuid);
|
if (appName != null) {
|
||||||
|
// This may be null if launched from the "Resume Session" PC context menu item
|
||||||
|
shortcutHelper.reportGameLaunched(computer, new NvApp(appName, appId, willStreamHdr));
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the MediaCodec helper before creating the decoder
|
// Initialize the MediaCodec helper before creating the decoder
|
||||||
GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
||||||
@@ -244,10 +301,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
// We must now ensure our display is compatible with HDR10
|
||||||
boolean foundHdr10 = false;
|
boolean foundHdr10 = false;
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
if (hdrCaps != null) {
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||||
LimeLog.info("Display supports HDR10");
|
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||||
foundHdr10 = true;
|
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||||
|
foundHdr10 = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +325,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
willStreamHdr = false;
|
willStreamHdr = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
|
// Check if the user has enabled performance stats overlay
|
||||||
|
if (prefConfig.enablePerfOverlay) {
|
||||||
|
performanceOverlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
decoderRenderer = new MediaCodecDecoderRenderer(
|
||||||
|
this,
|
||||||
|
prefConfig,
|
||||||
new CrashListener() {
|
new CrashListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyCrash(Exception e) {
|
public void notifyCrash(Exception e) {
|
||||||
@@ -281,8 +347,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
tombstonePrefs.getInt("CrashCount", 0),
|
tombstonePrefs.getInt("CrashCount", 0),
|
||||||
connMgr.isActiveNetworkMetered(),
|
connMgr.isActiveNetworkMetered(),
|
||||||
willStreamHdr,
|
willStreamHdr,
|
||||||
glPrefs.glRenderer
|
glPrefs.glRenderer,
|
||||||
);
|
this);
|
||||||
|
|
||||||
// Don't stream HDR if the decoder can't support it
|
// Don't stream HDR if the decoder can't support it
|
||||||
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
|
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
|
||||||
@@ -296,8 +362,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
|
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
|
||||||
if (!prefConfig.multiController && gamepadMask != 0) {
|
if (!prefConfig.multiController) {
|
||||||
// If any gamepads are present in non-MC mode, set only gamepad 1.
|
// Always set gamepad 1 present for when multi-controller is
|
||||||
|
// disabled for games that don't properly support detection
|
||||||
|
// of gamepads removed and replugged at runtime.
|
||||||
gamepadMask = 1;
|
gamepadMask = 1;
|
||||||
}
|
}
|
||||||
if (prefConfig.onscreenController) {
|
if (prefConfig.onscreenController) {
|
||||||
@@ -329,8 +397,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Hopefully, we can get rid of this once someone comes up with a better way
|
// Hopefully, we can get rid of this once someone comes up with a better way
|
||||||
// to track the state of the pipeline and time frames.
|
// to track the state of the pipeline and time frames.
|
||||||
int roundedRefreshRate = Math.round(displayRefreshRate);
|
int roundedRefreshRate = Math.round(displayRefreshRate);
|
||||||
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
|
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
|
||||||
if (roundedRefreshRate <= 49) {
|
if (prefConfig.unlockFps) {
|
||||||
|
// Use frame drops when rendering above the screen frame rate
|
||||||
|
decoderRenderer.enableLegacyFrameDropRendering();
|
||||||
|
LimeLog.info("Using drop mode for FPS > Hz");
|
||||||
|
}
|
||||||
|
else if (roundedRefreshRate <= 49) {
|
||||||
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
|
||||||
decoderRenderer.enableLegacyFrameDropRendering();
|
decoderRenderer.enableLegacyFrameDropRendering();
|
||||||
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
|
||||||
@@ -349,12 +422,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
.setApp(new NvApp(appName, appId, willStreamHdr))
|
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
|
||||||
.setBitrate(prefConfig.bitrate)
|
.setBitrate(prefConfig.bitrate)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||||
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
|
.setMaxPacketSize(1392)
|
||||||
.setRemote(remote)
|
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
|
||||||
.setHevcBitratePercentageMultiplier(75)
|
.setHevcBitratePercentageMultiplier(75)
|
||||||
.setHevcSupported(decoderRenderer.isHevcSupported())
|
.setHevcSupported(decoderRenderer.isHevcSupported())
|
||||||
.setEnableHdr(willStreamHdr)
|
.setEnableHdr(willStreamHdr)
|
||||||
@@ -366,7 +439,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Initialize the connection
|
// Initialize the connection
|
||||||
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
|
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert);
|
||||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
|
||||||
|
|
||||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||||
@@ -387,10 +460,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
if (prefConfig.onscreenController) {
|
if (prefConfig.onscreenController) {
|
||||||
// create virtual onscreen controller
|
// create virtual onscreen controller
|
||||||
virtualController = new VirtualController(conn,
|
virtualController = new VirtualController(controllerHandler,
|
||||||
(FrameLayout)streamView.getParent(),
|
(FrameLayout)streamView.getParent(),
|
||||||
this);
|
this);
|
||||||
virtualController.refreshLayout();
|
virtualController.refreshLayout();
|
||||||
|
virtualController.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefConfig.usbDriver) {
|
if (prefConfig.usbDriver) {
|
||||||
@@ -415,19 +489,65 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
streamView.getHolder().addCallback(this);
|
streamView.getHolder().addCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
|
if (virtualController != null) {
|
||||||
|
// Refresh layout of OSC for possible new screen size
|
||||||
|
virtualController.refreshLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide on-screen overlays in PiP mode
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (isInPictureInPictureMode()) {
|
||||||
|
isHidingOverlays = true;
|
||||||
|
|
||||||
|
if (virtualController != null) {
|
||||||
|
virtualController.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
performanceOverlayView.setVisibility(View.GONE);
|
||||||
|
notificationOverlayView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isHidingOverlays = false;
|
||||||
|
|
||||||
|
// Restore overlays to previous state when leaving PiP
|
||||||
|
|
||||||
|
if (virtualController != null) {
|
||||||
|
virtualController.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefConfig.enablePerfOverlay) {
|
||||||
|
performanceOverlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUserLeaveHint() {
|
public void onUserLeaveHint() {
|
||||||
super.onUserLeaveHint();
|
super.onUserLeaveHint();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (prefConfig.enablePip && connected) {
|
if (prefConfig.enablePip && connected) {
|
||||||
enterPictureInPictureMode(
|
try {
|
||||||
new PictureInPictureParams.Builder()
|
// This has thrown all sorts of weird exceptions on Samsung devices
|
||||||
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
|
// running Oreo. Just eat them and close gracefully on leave, rather
|
||||||
.setSourceRectHint(new Rect(
|
// than crashing.
|
||||||
streamView.getLeft(), streamView.getTop(),
|
enterPictureInPictureMode(
|
||||||
streamView.getRight(), streamView.getBottom()))
|
new PictureInPictureParams.Builder()
|
||||||
.build());
|
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
|
||||||
|
.setSourceRectHint(new Rect(
|
||||||
|
streamView.getLeft(), streamView.getTop(),
|
||||||
|
streamView.getRight(), streamView.getBottom()))
|
||||||
|
.build());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,8 +560,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Capture is lost when focus is lost, so it must be requested again
|
// Capture is lost when focus is lost, so it must be requested again
|
||||||
// when focus is regained.
|
// when focus is regained.
|
||||||
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
|
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
|
||||||
// Recapture the pointer if focus was regained
|
// Recapture the pointer if focus was regained. On Android Q,
|
||||||
streamView.requestPointerCapture();
|
// we have to delay a bit before requesting capture because otherwise
|
||||||
|
// we'll hit the "requestPointerCapture called for a window that has no focus"
|
||||||
|
// error and it will not actually capture the cursor.
|
||||||
|
Handler h = new Handler();
|
||||||
|
h.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
streamView.requestPointerCapture();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,8 +584,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Display.Mode bestMode = display.getMode();
|
Display.Mode bestMode = display.getMode();
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
|
||||||
candidate.getRefreshRate() < 63;
|
|
||||||
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||||
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||||
candidate.getPhysicalWidth() <= 4096;
|
candidate.getPhysicalWidth() <= 4096;
|
||||||
@@ -472,6 +600,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||||
|
if (prefConfig.fps <= 60) {
|
||||||
|
if (candidate.getRefreshRate() >= 63) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the refresh rate doesn't regress
|
// Make sure the refresh rate doesn't regress
|
||||||
if (!refreshRateOk) {
|
if (!refreshRateOk) {
|
||||||
continue;
|
continue;
|
||||||
@@ -489,12 +624,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||||
displayRefreshRate = bestMode.getRefreshRate();
|
displayRefreshRate = bestMode.getRefreshRate();
|
||||||
}
|
}
|
||||||
// On L, we can at least tell the OS that we want 60 Hz
|
// On L, we can at least tell the OS that we want a refresh rate
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
float bestRefreshRate = display.getRefreshRate();
|
float bestRefreshRate = display.getRefreshRate();
|
||||||
for (float candidate : display.getSupportedRefreshRates()) {
|
for (float candidate : display.getSupportedRefreshRates()) {
|
||||||
if (candidate > bestRefreshRate && candidate < 63) {
|
if (candidate > bestRefreshRate) {
|
||||||
LimeLog.info("Examining refresh rate: "+candidate);
|
LimeLog.info("Examining refresh rate: "+candidate);
|
||||||
|
|
||||||
|
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
|
||||||
|
if (prefConfig.fps <= 60) {
|
||||||
|
if (candidate >= 63) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bestRefreshRate = candidate;
|
bestRefreshRate = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,7 +761,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
inputManager.unregisterInputDeviceListener(controllerHandler);
|
inputManager.unregisterInputDeviceListener(controllerHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
wifiLock.release();
|
if (lowLatencyWifiLock != null) {
|
||||||
|
lowLatencyWifiLock.release();
|
||||||
|
}
|
||||||
|
if (highPerfWifiLock != null) {
|
||||||
|
highPerfWifiLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
if (connectedToUsbDriverService) {
|
if (connectedToUsbDriverService) {
|
||||||
// Unbind from the discovery service
|
// Unbind from the discovery service
|
||||||
@@ -636,6 +784,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
SpinnerDialog.closeDialogs(this);
|
SpinnerDialog.closeDialogs(this);
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
if (virtualController != null) {
|
||||||
|
virtualController.hide();
|
||||||
|
}
|
||||||
|
|
||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
int videoFormat = decoderRenderer.getActiveVideoFormat();
|
||||||
|
|
||||||
@@ -779,25 +931,30 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
|
return handleKeyDown(event) || super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleKeyDown(KeyEvent event) {
|
||||||
// Pass-through virtual navigation keys
|
// Pass-through virtual navigation keys
|
||||||
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a synthetic back button event that some Android OS versions
|
// Handle a synthetic back button event that some Android OS versions
|
||||||
// create as a result of a right-click.
|
// create as a result of a right-click. This event WILL repeat if
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
// the right mouse button is held down, so we ignore those.
|
||||||
|
if (!prefConfig.mouseNavButtons &&
|
||||||
|
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||||
|
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean handled = false;
|
boolean handled = false;
|
||||||
|
|
||||||
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
if (ControllerHandler.isGameControllerDevice(event.getDevice())) {
|
||||||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
|
||||||
if (detectedGamepad || (event.getDevice() == null ||
|
|
||||||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
|
||||||
)) {
|
|
||||||
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
|
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
|
||||||
// Otherwise, controller handler will eat keyboard d-pad events.
|
// Otherwise, controller handler will eat keyboard d-pad events.
|
||||||
handled = controllerHandler.handleButtonDown(event);
|
handled = controllerHandler.handleButtonDown(event);
|
||||||
@@ -807,20 +964,25 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = KeyboardTranslator.translate(event.getKeyCode());
|
short translated = KeyboardTranslator.translate(event.getKeyCode());
|
||||||
if (translated == 0) {
|
if (translated == 0) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let this method take duplicate key down events
|
// Let this method take duplicate key down events
|
||||||
if (handleSpecialKeys(keyCode, true)) {
|
if (handleSpecialKeys(event.getKeyCode(), true)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return super.onKeyDown(keyCode, event);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState(event));
|
byte modifiers = getModifierState(event);
|
||||||
|
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
|
||||||
|
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
|
||||||
|
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_DOWN, modifiers);
|
||||||
|
}
|
||||||
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -828,24 +990,28 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
|
return handleKeyUp(event) || super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleKeyUp(KeyEvent event) {
|
||||||
// Pass-through virtual navigation keys
|
// Pass-through virtual navigation keys
|
||||||
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a synthetic back button event that some Android OS versions
|
// Handle a synthetic back button event that some Android OS versions
|
||||||
// create as a result of a right-click.
|
// create as a result of a right-click.
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
if (!prefConfig.mouseNavButtons &&
|
||||||
|
(event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
|
||||||
|
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean handled = false;
|
boolean handled = false;
|
||||||
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
|
if (ControllerHandler.isGameControllerDevice(event.getDevice())) {
|
||||||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
|
||||||
if (detectedGamepad || (event.getDevice() == null ||
|
|
||||||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
|
||||||
)) {
|
|
||||||
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
|
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
|
||||||
// Otherwise, controller handler will eat keyboard d-pad events.
|
// Otherwise, controller handler will eat keyboard d-pad events.
|
||||||
handled = controllerHandler.handleButtonUp(event);
|
handled = controllerHandler.handleButtonUp(event);
|
||||||
@@ -855,19 +1021,26 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
// Try the keyboard handler
|
// Try the keyboard handler
|
||||||
short translated = KeyboardTranslator.translate(event.getKeyCode());
|
short translated = KeyboardTranslator.translate(event.getKeyCode());
|
||||||
if (translated == 0) {
|
if (translated == 0) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleSpecialKeys(keyCode, false)) {
|
if (handleSpecialKeys(event.getKeyCode(), false)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass through keyboard input if we're not grabbing
|
// Pass through keyboard input if we're not grabbing
|
||||||
if (!grabbedInput) {
|
if (!grabbedInput) {
|
||||||
return super.onKeyUp(keyCode, event);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, getModifierState(event));
|
byte modifiers = getModifierState(event);
|
||||||
|
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
|
||||||
|
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
|
||||||
|
}
|
||||||
|
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, modifiers);
|
||||||
|
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
|
||||||
|
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_UP, getModifierState(event));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -907,6 +1080,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
{
|
{
|
||||||
// This case is for mice
|
// This case is for mice
|
||||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||||
|
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
|
||||||
(event.getPointerCount() >= 1 &&
|
(event.getPointerCount() >= 1 &&
|
||||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
||||||
{
|
{
|
||||||
@@ -922,6 +1096,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||||
conn.sendMouseScroll(vScrollClicks);
|
conn.sendMouseScroll(vScrollClicks);
|
||||||
}
|
}
|
||||||
|
else if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER ||
|
||||||
|
event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
// On some devices (Galaxy S8 without Oreo pointer capture), we can
|
||||||
|
// get spurious ACTION_HOVER_ENTER events when right clicking with
|
||||||
|
// incorrect X and Y coordinates. Just eat this event without processing it.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||||
@@ -950,6 +1131,26 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prefConfig.mouseNavButtons) {
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||||
|
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
|
||||||
|
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get relative axis values if we can
|
// Get relative axis values if we can
|
||||||
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||||
// Send the deltas straight from the motion event
|
// Send the deltas straight from the motion event
|
||||||
@@ -961,13 +1162,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
lastMouseX = (int)event.getX();
|
lastMouseX = (int)event.getX();
|
||||||
lastMouseY = (int)event.getY();
|
lastMouseY = (int)event.getY();
|
||||||
}
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// We get a normal (non-relative) MotionEvent when starting pointer capture to synchronize the
|
||||||
|
// location of the cursor with our app. We don't want this, so we must discard this event.
|
||||||
|
lastMouseX = (int)event.getX();
|
||||||
|
lastMouseY = (int)event.getY();
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// First process the history
|
// Don't process the history. We just want the current position now.
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
|
||||||
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now process the current values
|
|
||||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1115,10 +1317,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageStarting(String stage) {
|
public void stageStarting(final String stage) {
|
||||||
if (spinner != null) {
|
runOnUiThread(new Runnable() {
|
||||||
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage);
|
@Override
|
||||||
}
|
public void run() {
|
||||||
|
if (spinner != null) {
|
||||||
|
spinner.setMessage(getResources().getString(R.string.conn_starting) + " " + stage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1129,6 +1336,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
if (connecting || connected) {
|
if (connecting || connected) {
|
||||||
connecting = connected = false;
|
connecting = connected = false;
|
||||||
|
|
||||||
|
controllerHandler.stop();
|
||||||
|
|
||||||
// Stop may take a few hundred ms to do some network I/O to tell
|
// Stop may take a few hundred ms to do some network I/O to tell
|
||||||
// the server we're going away and clean up. Let it run in a separate
|
// the server we're going away and clean up. Let it run in a separate
|
||||||
// thread to keep things smooth for the UI. Inside moonlight-common,
|
// thread to keep things smooth for the UI. Inside moonlight-common,
|
||||||
@@ -1143,70 +1352,123 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stageFailed(String stage, long errorCode) {
|
public void stageFailed(final String stage, final long errorCode) {
|
||||||
if (spinner != null) {
|
runOnUiThread(new Runnable() {
|
||||||
spinner.dismiss();
|
@Override
|
||||||
spinner = null;
|
public void run() {
|
||||||
}
|
if (spinner != null) {
|
||||||
|
spinner.dismiss();
|
||||||
|
spinner = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Enable cursor visibility again
|
if (!displayedFailureDialog) {
|
||||||
inputCaptureProvider.disableCapture();
|
displayedFailureDialog = true;
|
||||||
|
LimeLog.severe(stage + " failed: " + errorCode);
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
// If video initialization failed and the surface is still valid, display extra information for the user
|
||||||
displayedFailureDialog = true;
|
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
||||||
LimeLog.severe(stage+" failed: "+errorCode);
|
|
||||||
|
|
||||||
// If video initialization failed and the surface is still valid, display extra information for the user
|
|
||||||
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
|
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
|
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
|
||||||
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
|
getResources().getString(R.string.conn_error_msg) + " " + stage, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionTerminated(long errorCode) {
|
public void connectionTerminated(final long errorCode) {
|
||||||
// Enable cursor visibility again
|
runOnUiThread(new Runnable() {
|
||||||
inputCaptureProvider.disableCapture();
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Let the display go to sleep now
|
||||||
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
if (!displayedFailureDialog) {
|
// Enable cursor visibility again
|
||||||
displayedFailureDialog = true;
|
inputCaptureProvider.disableCapture();
|
||||||
LimeLog.severe("Connection terminated: "+errorCode);
|
|
||||||
stopConnection();
|
|
||||||
|
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
|
if (!displayedFailureDialog) {
|
||||||
getResources().getString(R.string.conn_terminated_msg), true);
|
displayedFailureDialog = true;
|
||||||
}
|
LimeLog.severe("Connection terminated: " + errorCode);
|
||||||
|
stopConnection();
|
||||||
|
|
||||||
|
// Display the error dialog if it was an unexpected termination.
|
||||||
|
// Otherwise, just finish the activity immediately.
|
||||||
|
if (errorCode != 0) {
|
||||||
|
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
|
||||||
|
getResources().getString(R.string.conn_terminated_msg), true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionStatusUpdate(final int connectionStatus) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (prefConfig.disableWarnings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionStatus == MoonBridge.CONN_STATUS_POOR) {
|
||||||
|
if (prefConfig.bitrate > 5000) {
|
||||||
|
notificationOverlayView.setText(getResources().getString(R.string.slow_connection_msg));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notificationOverlayView.setText(getResources().getString(R.string.poor_connection_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedNotificationOverlayVisibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
else if (connectionStatus == MoonBridge.CONN_STATUS_OKAY) {
|
||||||
|
requestedNotificationOverlayVisibility = View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHidingOverlays) {
|
||||||
|
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionStarted() {
|
public void connectionStarted() {
|
||||||
if (spinner != null) {
|
|
||||||
spinner.dismiss();
|
|
||||||
spinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
connecting = false;
|
|
||||||
connected = true;
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Hide the mouse cursor now. Doing it before
|
if (spinner != null) {
|
||||||
// dismissing the spinner seems to be undone
|
spinner.dismiss();
|
||||||
// when the spinner gets displayed.
|
spinner = null;
|
||||||
inputCaptureProvider.enableCapture();
|
}
|
||||||
|
|
||||||
|
connected = true;
|
||||||
|
connecting = false;
|
||||||
|
|
||||||
|
// Hide the mouse cursor now after a short delay.
|
||||||
|
// Doing it before dismissing the spinner seems to be undone
|
||||||
|
// when the spinner gets displayed. On Android Q, even now
|
||||||
|
// is too early to capture. We will delay a second to allow
|
||||||
|
// the spinner to dismiss before capturing.
|
||||||
|
Handler h = new Handler();
|
||||||
|
h.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
inputCaptureProvider.enableCapture();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Keep the display on
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
|
hideSystemUi(1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
hideSystemUi(1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1232,13 +1494,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
public void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||||
|
LimeLog.info(String.format((Locale)null, "Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor));
|
||||||
|
|
||||||
|
controllerHandler.handleRumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||||
if (!connected && !connecting) {
|
if (!surfaceCreated) {
|
||||||
connecting = true;
|
throw new IllegalStateException("Surface changed before creation!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attemptedConnection) {
|
||||||
|
attemptedConnection = true;
|
||||||
|
|
||||||
decoderRenderer.setRenderTarget(holder);
|
decoderRenderer.setRenderTarget(holder);
|
||||||
conn.start(PlatformBinding.getAudioRenderer(), decoderRenderer, Game.this);
|
conn.start(PlatformBinding.getAudioRenderer(), decoderRenderer, Game.this);
|
||||||
@@ -1246,12 +1515,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
// Let the decoder know immediately that the surface is gone
|
surfaceCreated = true;
|
||||||
decoderRenderer.prepareForStop();
|
}
|
||||||
|
|
||||||
if (connected) {
|
@Override
|
||||||
stopConnection();
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
if (!surfaceCreated) {
|
||||||
|
throw new IllegalStateException("Surface destroyed before creation!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attemptedConnection) {
|
||||||
|
// Let the decoder know immediately that the surface is gone
|
||||||
|
decoderRenderer.prepareForStop();
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
stopConnection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,6 +1555,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
case EvdevListener.BUTTON_RIGHT:
|
case EvdevListener.BUTTON_RIGHT:
|
||||||
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
|
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
|
||||||
break;
|
break;
|
||||||
|
case EvdevListener.BUTTON_X1:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_X1;
|
||||||
|
break;
|
||||||
|
case EvdevListener.BUTTON_X2:
|
||||||
|
buttonIndex = MouseButtonPacket.BUTTON_X2;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LimeLog.warning("Unhandled button: "+buttonId);
|
LimeLog.warning("Unhandled button: "+buttonId);
|
||||||
return;
|
return;
|
||||||
@@ -1333,4 +1619,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
hideSystemUi(2000);
|
hideSystemUi(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPerfUpdate(final String text) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
performanceOverlayView.setText(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.PcGridAdapter;
|
import com.limelight.grid.PcGridAdapter;
|
||||||
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
@@ -28,6 +29,7 @@ import com.limelight.utils.ShortcutHelper;
|
|||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.ActivityManager;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -52,6 +54,8 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig;
|
import javax.microedition.khronos.egl.EGLConfig;
|
||||||
import javax.microedition.khronos.opengles.GL10;
|
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 DELETE_ID = 5;
|
||||||
private final static int RESUME_ID = 6;
|
private final static int RESUME_ID = 6;
|
||||||
private final static int QUIT_ID = 7;
|
private final static int QUIT_ID = 7;
|
||||||
|
private final static int VIEW_DETAILS_ID = 8;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -311,8 +316,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
|
|
||||||
// Inflate the context menu
|
// Inflate the context menu
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
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, 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));
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
@@ -332,6 +337,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// it with delete which actually work
|
// 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, 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
|
@Override
|
||||||
@@ -343,7 +349,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
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();
|
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -369,9 +376,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
computer.serverCert,
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
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
|
// Don't display any toast, but open the app list
|
||||||
message = null;
|
message = null;
|
||||||
success = true;
|
success = true;
|
||||||
@@ -383,21 +390,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||||
|
|
||||||
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
PairingManager pm = httpConn.getPairingManager();
|
||||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
|
||||||
|
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||||
|
if (pairState == PairState.PIN_WRONG) {
|
||||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
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);
|
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);
|
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
|
// Just navigate to the app view without displaying a toast
|
||||||
message = null;
|
message = null;
|
||||||
success = true;
|
success = true;
|
||||||
|
|
||||||
|
// Pin this certificate for later HTTPS use
|
||||||
|
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
||||||
|
|
||||||
// Invalidate reachability information after pairing to force
|
// Invalidate reachability information after pairing to force
|
||||||
// a refresh before reading pair state again
|
// a refresh before reading pair state again
|
||||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
@@ -411,7 +423,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
message = getResources().getString(R.string.error_unknown_host);
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = getResources().getString(R.string.error_404);
|
message = getResources().getString(R.string.error_404);
|
||||||
} catch (Exception e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
}
|
}
|
||||||
@@ -429,7 +441,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
if (toastSuccess) {
|
if (toastSuccess) {
|
||||||
// Open the app list after a successful pairing attempt
|
// Open the app list after a successful pairing attempt
|
||||||
doAppList(computer);
|
doAppList(computer, true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Start polling again if we're still in the foreground
|
// Start polling again if we're still in the foreground
|
||||||
@@ -452,7 +464,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -476,7 +487,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doUnpair(final ComputerDetails computer) {
|
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();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -494,7 +506,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
try {
|
try {
|
||||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
PlatformBinding.getDeviceName(),
|
computer.serverCert,
|
||||||
PlatformBinding.getCryptoProvider(PcView.this));
|
PlatformBinding.getCryptoProvider(PcView.this));
|
||||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||||
httpConn.unpair();
|
httpConn.unpair();
|
||||||
@@ -512,8 +524,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
message = getResources().getString(R.string.error_unknown_host);
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = getResources().getString(R.string.error_404);
|
message = getResources().getString(R.string.error_404);
|
||||||
} catch (Exception e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String toastMessage = message;
|
final String toastMessage = message;
|
||||||
@@ -527,8 +540,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doAppList(ComputerDetails computer) {
|
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -539,7 +552,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
Intent i = new Intent(this, AppView.class);
|
Intent i = new Intent(this, AppView.class);
|
||||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
|
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||||
|
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,16 +575,24 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case DELETE_ID:
|
case DELETE_ID:
|
||||||
if (managerBinder == null) {
|
if (ActivityManager.isUserAMonkey()) {
|
||||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
LimeLog.info("Ignoring delete PC request from monkey");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
managerBinder.removeComputer(computer.details.name);
|
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
|
||||||
removeComputer(computer.details);
|
@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;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case APP_LIST_ID:
|
||||||
doAppList(computer.details);
|
doAppList(computer.details, false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case RESUME_ID:
|
case RESUME_ID:
|
||||||
@@ -592,25 +614,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
ServerHelper.doQuit(PcView.this,
|
ServerHelper.doQuit(PcView.this, computer.details,
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
|
||||||
new NvApp("app", 0, false), managerBinder, null);
|
new NvApp("app", 0, false), managerBinder, null);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case VIEW_DETAILS_ID:
|
||||||
|
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeComputer(ComputerDetails details) {
|
private void removeComputer(ComputerDetails details) {
|
||||||
|
managerBinder.removeComputer(details);
|
||||||
|
|
||||||
|
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
|
||||||
|
|
||||||
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||||
|
|
||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
// Disable or delete shortcuts referencing this PC
|
// Disable or delete shortcuts referencing this PC
|
||||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
shortcutHelper.disableComputerShortcut(details,
|
||||||
getResources().getString(R.string.scut_deleted_pc));
|
getResources().getString(R.string.scut_deleted_pc));
|
||||||
|
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
@@ -641,7 +670,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
// Add a launcher shortcut for this PC
|
// Add a launcher shortcut for this PC
|
||||||
if (details.pairState == PairState.PAIRED) {
|
if (details.pairState == PairState.PAIRED) {
|
||||||
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
shortcutHelper.createAppViewShortcutForOnlineHost(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
@@ -675,18 +704,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
|
||||||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
computer.details.state == ComputerDetails.State.OFFLINE) {
|
||||||
// Open the context menu if a PC is offline or refreshing
|
// Open the context menu if a PC is offline or refreshing
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else if (computer.details.pairState != PairState.PAIRED) {
|
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||||
// Pair an unpaired machine by default
|
// Pair an unpaired machine by default
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
} else {
|
} else {
|
||||||
doAppList(computer.details);
|
doAppList(computer.details, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
UiHelper.applyStatusBarPadding(listView);
|
||||||
registerForContextMenu(listView);
|
registerForContextMenu(listView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,8 +168,17 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playDecodedAudio(byte[] audioData) {
|
public void playDecodedAudio(short[] audioData) {
|
||||||
track.write(audioData, 0, audioData.length);
|
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||||
|
if (MoonBridge.getPendingAudioFrames() < 8) {
|
||||||
|
// 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 many pending audio frames: " + MoonBridge.getPendingAudioFrames());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import java.security.KeyFactory;
|
|||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.Provider;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -54,10 +53,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
|
|
||||||
private static final Object globalCryptoLock = new Object();
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
static {
|
private static final Provider bcProvider = new BouncyCastleProvider();
|
||||||
// Install the Bouncy Castle provider
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
public AndroidCryptoProvider(Context c) {
|
public AndroidCryptoProvider(Context c) {
|
||||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
@@ -96,10 +92,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
pemCertBytes = certBytes;
|
pemCertBytes = certBytes;
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
// May happen if the cert is corrupt
|
// May happen if the cert is corrupt
|
||||||
@@ -113,10 +109,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
// May happen if the key is corrupt
|
// May happen if the key is corrupt
|
||||||
LimeLog.warning("Corrupted key");
|
LimeLog.warning("Corrupted key");
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchProviderException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -129,17 +121,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
|
|
||||||
KeyPair keyPair;
|
KeyPair keyPair;
|
||||||
try {
|
try {
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchProviderException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
@@ -160,13 +148,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
|||||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||||
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Nothing should go wrong here
|
// Nothing should go wrong here
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return false;
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Generated a new key pair");
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import android.content.Context;
|
|||||||
import android.hardware.input.InputManager;
|
import android.hardware.input.InputManager;
|
||||||
import android.hardware.usb.UsbDevice;
|
import android.hardware.usb.UsbDevice;
|
||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
@@ -13,6 +17,7 @@ import android.view.MotionEvent;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.input.driver.AbstractController;
|
||||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||||
import com.limelight.binding.input.driver.UsbDriverService;
|
import com.limelight.binding.input.driver.UsbDriverService;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
@@ -22,6 +27,7 @@ import com.limelight.preferences.PreferenceConfiguration;
|
|||||||
import com.limelight.ui.GameGestures;
|
import com.limelight.ui.GameGestures;
|
||||||
import com.limelight.utils.Vector2d;
|
import com.limelight.utils.Vector2d;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
@@ -49,6 +55,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
private final double stickDeadzone;
|
private final double stickDeadzone;
|
||||||
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
||||||
private final GameGestures gestures;
|
private final GameGestures gestures;
|
||||||
|
private final Vibrator deviceVibrator;
|
||||||
private boolean hasGameController;
|
private boolean hasGameController;
|
||||||
|
|
||||||
private final PreferenceConfiguration prefConfig;
|
private final PreferenceConfiguration prefConfig;
|
||||||
@@ -59,10 +66,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.gestures = gestures;
|
this.gestures = gestures;
|
||||||
this.prefConfig = prefConfig;
|
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.
|
// is required for controller batching support to work.
|
||||||
int deadzonePercentage = 10;
|
int deadzonePercentage = 7;
|
||||||
|
|
||||||
int[] ids = InputDevice.getDeviceIds();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int id : ids) {
|
for (int id : ids) {
|
||||||
@@ -149,6 +157,42 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
onInputDeviceAdded(deviceId);
|
onInputDeviceAdded(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||||
|
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
|
||||||
|
|
||||||
|
if (deviceContext.vibrator != null) {
|
||||||
|
deviceContext.vibrator.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceVibrator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasJoystickAxes(InputDevice device) {
|
||||||
|
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
|
||||||
|
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
|
||||||
|
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_Y) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasGamepadButtons(InputDevice device) {
|
||||||
|
return (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGameControllerDevice(InputDevice device) {
|
||||||
|
if (device == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasJoystickAxes(device) || hasGamepadButtons(device)) {
|
||||||
|
// Has real joystick axes or gamepad buttons
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
|
||||||
|
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
|
||||||
|
}
|
||||||
|
|
||||||
public static short getAttachedControllerMask(Context context) {
|
public static short getAttachedControllerMask(Context context) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
short mask = 0;
|
short mask = 0;
|
||||||
@@ -161,7 +205,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0) {
|
if (hasJoystickAxes(dev)) {
|
||||||
LimeLog.info("Counting InputDevice: "+dev.getName());
|
LimeLog.info("Counting InputDevice: "+dev.getName());
|
||||||
mask |= 1 << count++;
|
mask |= 1 << count++;
|
||||||
}
|
}
|
||||||
@@ -180,6 +224,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
|
||||||
|
LimeLog.info("Counting OSC gamepad");
|
||||||
|
mask |= 1;
|
||||||
|
}
|
||||||
|
|
||||||
LimeLog.info("Enumerated "+count+" gamepads");
|
LimeLog.info("Enumerated "+count+" gamepads");
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
@@ -272,10 +321,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.assignedControllerNumber = true;
|
context.assignedControllerNumber = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
|
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
|
||||||
UsbDeviceContext context = new UsbDeviceContext();
|
UsbDeviceContext context = new UsbDeviceContext();
|
||||||
|
|
||||||
context.id = deviceId;
|
context.id = device.getControllerId();
|
||||||
|
context.device = device;
|
||||||
|
|
||||||
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
context.leftStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
context.rightStickDeadzoneRadius = (float) stickDeadzone;
|
||||||
@@ -284,6 +334,85 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isExternal(InputDevice dev) {
|
||||||
|
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
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldIgnoreBack(InputDevice dev) {
|
||||||
|
String devName = dev.getName();
|
||||||
|
|
||||||
|
// The Serval has a Select button but the framework doesn't
|
||||||
|
// know about that because it uses a non-standard scancode.
|
||||||
|
if (devName.contains("Razer Serval")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, dynamically try to determine whether we should allow this
|
||||||
|
// back button to function for navigation.
|
||||||
|
//
|
||||||
|
// First, check if this is an internal device we're being called on.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !isExternal(dev)) {
|
||||||
|
InputManager im = (InputManager) activityContext.getSystemService(Context.INPUT_SERVICE);
|
||||||
|
|
||||||
|
boolean foundInternalGamepad = false;
|
||||||
|
boolean foundInternalSelect = false;
|
||||||
|
for (int id : im.getInputDeviceIds()) {
|
||||||
|
InputDevice currentDev = im.getInputDevice(id);
|
||||||
|
|
||||||
|
// Ignore external devices
|
||||||
|
if (currentDev == null || isExternal(currentDev)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]) {
|
||||||
|
foundInternalSelect = true;
|
||||||
|
}
|
||||||
|
if (keys[1]) {
|
||||||
|
foundInternalGamepad = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the back button to function for navigation if we either:
|
||||||
|
// a) have no internal gamepad (most phones)
|
||||||
|
// b) have an internal gamepad but also have an internal select button (GPD XD)
|
||||||
|
// but not:
|
||||||
|
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
|
||||||
|
return !foundInternalGamepad || foundInternalSelect;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
|
||||||
InputDeviceContext context = new InputDeviceContext();
|
InputDeviceContext context = new InputDeviceContext();
|
||||||
String devName = dev.getName();
|
String devName = dev.getName();
|
||||||
@@ -294,6 +423,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.name = devName;
|
context.name = devName;
|
||||||
context.id = dev.getId();
|
context.id = dev.getId();
|
||||||
|
|
||||||
|
if (dev.getVibrator().hasVibrator()) {
|
||||||
|
context.vibrator = dev.getVibrator();
|
||||||
|
}
|
||||||
|
|
||||||
context.leftStickXAxis = MotionEvent.AXIS_X;
|
context.leftStickXAxis = MotionEvent.AXIS_X;
|
||||||
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
context.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||||
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
|
||||||
@@ -416,6 +549,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.ignoreBack = shouldIgnoreBack(dev);
|
||||||
|
|
||||||
if (devName != null) {
|
if (devName != null) {
|
||||||
// For the Nexus Player (and probably other ATV devices), we should
|
// For the Nexus Player (and probably other ATV devices), we should
|
||||||
// use the back button as start since it doesn't have a start/menu button
|
// use the back button as start since it doesn't have a start/menu button
|
||||||
@@ -435,30 +570,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// so we increase the deadzone on them to minimize this
|
// so we increase the deadzone on them to minimize this
|
||||||
context.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
// Classify this device as a remote by name
|
|
||||||
else if (devName.toLowerCase().contains("remote")) {
|
|
||||||
// It's only a remote if it doesn't any sticks
|
|
||||||
if (!context.hasJoystickAxes) {
|
|
||||||
context.ignoreBack = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SHIELD controllers will use small stick deadzones
|
// SHIELD controllers will use small stick deadzones
|
||||||
else if (devName.contains("SHIELD")) {
|
else if (devName.contains("SHIELD")) {
|
||||||
context.leftStickDeadzoneRadius = 0.07f;
|
context.leftStickDeadzoneRadius = 0.07f;
|
||||||
context.rightStickDeadzoneRadius = 0.07f;
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
}
|
}
|
||||||
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
|
||||||
// back presses on this device. The Goodix buttons on the Nokia 6 also appear
|
|
||||||
// non-virtual so we'll ignore those too.
|
|
||||||
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey") ||
|
|
||||||
devName.equals("goodix_fp")) {
|
|
||||||
context.ignoreBack = true;
|
|
||||||
}
|
|
||||||
// The Serval has a couple of unknown buttons that are start and select. It also has
|
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||||
// a back button which we want to ignore since there's already a select button.
|
// a back button which we want to ignore since there's already a select button.
|
||||||
else if (devName.contains("Razer Serval")) {
|
else if (devName.contains("Razer Serval")) {
|
||||||
context.isServal = true;
|
context.isServal = true;
|
||||||
context.ignoreBack = true;
|
|
||||||
}
|
}
|
||||||
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
||||||
// However, Microsoft released a firmware update with no change to VID/PID
|
// However, Microsoft released a firmware update with no change to VID/PID
|
||||||
@@ -528,7 +648,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
private short getActiveControllerMask() {
|
private short getActiveControllerMask() {
|
||||||
if (prefConfig.multiController) {
|
if (prefConfig.multiController) {
|
||||||
return (short)(currentControllers | initialControllers);
|
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Only Player 1 is active with multi-controller disabled
|
// Only Player 1 is active with multi-controller disabled
|
||||||
@@ -835,16 +955,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
// to normal behavior, so ignore triggersIdleNegative for each trigger until
|
// to normal behavior, so ignore triggersIdleNegative for each trigger until
|
||||||
// first touch.
|
// first touch.
|
||||||
if (lt != 0) {
|
if (lt != 0) {
|
||||||
context.leftTriggerUsed = true;
|
context.leftTriggerAxisUsed = true;
|
||||||
}
|
}
|
||||||
if (rt != 0) {
|
if (rt != 0) {
|
||||||
context.rightTriggerUsed = true;
|
context.rightTriggerAxisUsed = true;
|
||||||
}
|
}
|
||||||
if (context.triggersIdleNegative) {
|
if (context.triggersIdleNegative) {
|
||||||
if (context.leftTriggerUsed) {
|
if (context.leftTriggerAxisUsed) {
|
||||||
lt = (lt + 1) / 2;
|
lt = (lt + 1) / 2;
|
||||||
}
|
}
|
||||||
if (context.rightTriggerUsed) {
|
if (context.rightTriggerAxisUsed) {
|
||||||
rt = (rt + 1) / 2;
|
rt = (rt + 1) / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -957,6 +1077,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) {
|
public boolean handleButtonUp(KeyEvent event) {
|
||||||
InputDeviceContext context = getContextForEvent(event);
|
InputDeviceContext context = getContextForEvent(event);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
@@ -1040,15 +1248,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
if (context.leftTriggerAxis >= 0) {
|
if (context.leftTriggerAxisUsed) {
|
||||||
// Suppress this digital event if an analog trigger is present
|
// Suppress this digital event if an analog trigger is active
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
context.leftTrigger = 0;
|
context.leftTrigger = 0;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
if (context.rightTriggerAxis >= 0) {
|
if (context.rightTriggerAxisUsed) {
|
||||||
// Suppress this digital event if an analog trigger is present
|
// Suppress this digital event if an analog trigger is active
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
context.rightTrigger = 0;
|
context.rightTrigger = 0;
|
||||||
@@ -1160,15 +1368,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||||
if (context.leftTriggerAxis >= 0) {
|
if (context.leftTriggerAxisUsed) {
|
||||||
// Suppress this digital event if an analog trigger is present
|
// Suppress this digital event if an analog trigger is active
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
context.leftTrigger = (byte)0xFF;
|
context.leftTrigger = (byte)0xFF;
|
||||||
break;
|
break;
|
||||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||||
if (context.rightTriggerAxis >= 0) {
|
if (context.rightTriggerAxisUsed) {
|
||||||
// Suppress this digital event if an analog trigger is present
|
// Suppress this digital event if an analog trigger is active
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
context.rightTrigger = (byte)0xFF;
|
context.rightTrigger = (byte)0xFF;
|
||||||
@@ -1208,12 +1416,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
return true;
|
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
|
@Override
|
||||||
public void reportControllerState(int controllerId, short buttonFlags,
|
public void reportControllerState(int controllerId, short buttonFlags,
|
||||||
float leftStickX, float leftStickY,
|
float leftStickX, float leftStickY,
|
||||||
float rightStickX, float rightStickY,
|
float rightStickX, float rightStickY,
|
||||||
float leftTrigger, float rightTrigger) {
|
float leftTrigger, float rightTrigger) {
|
||||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
GenericControllerContext context = usbDeviceContexts.get(controllerId);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1248,19 +1474,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceRemoved(int controllerId) {
|
public void deviceRemoved(AbstractController controller) {
|
||||||
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
|
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
LimeLog.info("Removed controller: "+controllerId);
|
LimeLog.info("Removed controller: "+controller.getControllerId());
|
||||||
releaseControllerNumber(context);
|
releaseControllerNumber(context);
|
||||||
usbDeviceContexts.remove(controllerId);
|
usbDeviceContexts.remove(controller.getControllerId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceAdded(int controllerId) {
|
public void deviceAdded(AbstractController controller) {
|
||||||
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
|
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
|
||||||
usbDeviceContexts.put(controllerId, context);
|
usbDeviceContexts.put(controller.getControllerId(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenericControllerContext {
|
class GenericControllerContext {
|
||||||
@@ -1289,6 +1515,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
|
|
||||||
class InputDeviceContext extends GenericControllerContext {
|
class InputDeviceContext extends GenericControllerContext {
|
||||||
public String name;
|
public String name;
|
||||||
|
public Vibrator vibrator;
|
||||||
|
|
||||||
public int leftStickXAxis = -1;
|
public int leftStickXAxis = -1;
|
||||||
public int leftStickYAxis = -1;
|
public int leftStickYAxis = -1;
|
||||||
@@ -1299,7 +1526,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public int leftTriggerAxis = -1;
|
public int leftTriggerAxis = -1;
|
||||||
public int rightTriggerAxis = -1;
|
public int rightTriggerAxis = -1;
|
||||||
public boolean triggersIdleNegative;
|
public boolean triggersIdleNegative;
|
||||||
public boolean leftTriggerUsed, rightTriggerUsed;
|
public boolean leftTriggerAxisUsed, rightTriggerAxisUsed;
|
||||||
|
|
||||||
public int hatXAxis = -1;
|
public int hatXAxis = -1;
|
||||||
public int hatYAxis = -1;
|
public int hatYAxis = -1;
|
||||||
@@ -1326,5 +1553,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
|||||||
public long startDownTime = 0;
|
public long startDownTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UsbDeviceContext extends GenericControllerContext {}
|
class UsbDeviceContext extends GenericControllerContext {
|
||||||
|
public AbstractController device;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,21 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_BACK_QUOTE = 192;
|
public static final int VK_BACK_QUOTE = 192;
|
||||||
public static final int VK_QUOTE = 222;
|
public static final int VK_QUOTE = 222;
|
||||||
public static final int VK_PAUSE = 19;
|
public static final int VK_PAUSE = 19;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates the given keycode and returns the GFE keycode
|
* Translates the given keycode and returns the GFE keycode
|
||||||
* @param keycode the code to be translated
|
* @param keycode the code to be translated
|
||||||
@@ -116,7 +130,8 @@ public class KeyboardTranslator {
|
|||||||
case KeyEvent.KEYCODE_ENTER:
|
case KeyEvent.KEYCODE_ENTER:
|
||||||
translated = 0x0d;
|
translated = 0x0d;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
case KeyEvent.KEYCODE_EQUALS:
|
case KeyEvent.KEYCODE_EQUALS:
|
||||||
translated = 0xbb;
|
translated = 0xbb;
|
||||||
break;
|
break;
|
||||||
@@ -257,7 +272,19 @@ public class KeyboardTranslator {
|
|||||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||||
translated = 0x6E;
|
translated = 0x6E;
|
||||||
break;
|
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:
|
default:
|
||||||
System.out.println("No key for "+keycode);
|
System.out.println("No key for "+keycode);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+10
-2
@@ -43,11 +43,19 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
return event.getX();
|
float x = event.getX();
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
x += event.getHistoricalX(i);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
return event.getY();
|
float y = event.getY();
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
y += event.getHistoricalY(i);
|
||||||
|
}
|
||||||
|
return y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,13 @@ public abstract class AbstractController {
|
|||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||||
|
|
||||||
protected void notifyDeviceRemoved() {
|
protected void notifyDeviceRemoved() {
|
||||||
listener.deviceRemoved(deviceId);
|
listener.deviceRemoved(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyDeviceAdded() {
|
protected void notifyDeviceAdded() {
|
||||||
listener.deviceAdded(deviceId);
|
listener.deviceAdded(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ public abstract class AbstractXboxController extends AbstractController {
|
|||||||
private Thread createInputThread() {
|
private Thread createInputThread() {
|
||||||
return new Thread() {
|
return new Thread() {
|
||||||
public void run() {
|
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) {
|
while (!isInterrupted() && !stopped) {
|
||||||
byte[] buffer = new byte[64];
|
byte[] buffer = new byte[64];
|
||||||
|
|
||||||
@@ -114,9 +126,6 @@ public abstract class AbstractXboxController extends AbstractController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report that we're added _before_ starting the input thread
|
|
||||||
notifyDeviceAdded();
|
|
||||||
|
|
||||||
// Start listening for controller input
|
// Start listening for controller input
|
||||||
inputThread = createInputThread();
|
inputThread = createInputThread();
|
||||||
inputThread.start();
|
inputThread.start();
|
||||||
@@ -131,6 +140,9 @@ public abstract class AbstractXboxController extends AbstractController {
|
|||||||
|
|
||||||
stopped = true;
|
stopped = true;
|
||||||
|
|
||||||
|
// Cancel any rumble effects
|
||||||
|
rumble((short)0, (short)0);
|
||||||
|
|
||||||
// Stop the input thread
|
// Stop the input thread
|
||||||
if (inputThread != null) {
|
if (inputThread != null) {
|
||||||
inputThread.interrupt();
|
inputThread.interrupt();
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
|
|||||||
float rightStickX, float rightStickY,
|
float rightStickX, float rightStickY,
|
||||||
float leftTrigger, float rightTrigger);
|
float leftTrigger, float rightTrigger);
|
||||||
|
|
||||||
void deviceRemoved(int controllerId);
|
void deviceRemoved(AbstractController controller);
|
||||||
void deviceAdded(int controllerId);
|
void deviceAdded(AbstractController controller);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceRemoved(int controllerId) {
|
public void deviceRemoved(AbstractController controller) {
|
||||||
// Remove the the controller from our list (if not removed already)
|
// Remove the the controller from our list (if not removed already)
|
||||||
for (AbstractController controller : controllers) {
|
controllers.remove(controller);
|
||||||
if (controller.getControllerId() == controllerId) {
|
|
||||||
controllers.remove(controller);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call through to the client's listener
|
// Call through to the client's listener
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.deviceRemoved(controllerId);
|
listener.deviceRemoved(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deviceAdded(int controllerId) {
|
public void deviceAdded(AbstractController controller) {
|
||||||
// Call through to the client's listener
|
// Call through to the client's listener
|
||||||
if (listener != null) {
|
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
|
// Report all controllerMap that already exist
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
for (AbstractController controller : controllers) {
|
for (AbstractController controller : controllers) {
|
||||||
listener.deviceAdded(controller.getControllerId());
|
listener.deviceAdded(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||||
|
|
||||||
private static final int[] SUPPORTED_VENDORS = {
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x0079, // GPD Win 2
|
||||||
0x044f, // Thrustmaster
|
0x044f, // Thrustmaster
|
||||||
0x045e, // Microsoft
|
0x045e, // Microsoft
|
||||||
0x046d, // Logitech
|
0x046d, // Logitech
|
||||||
@@ -23,6 +24,7 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
0x07ff, // Mad Catz
|
0x07ff, // Mad Catz
|
||||||
0x0e6f, // Unknown
|
0x0e6f, // Unknown
|
||||||
0x0f0d, // Hori
|
0x0f0d, // Hori
|
||||||
|
0x1038, // SteelSeries
|
||||||
0x11c9, // Nacon
|
0x11c9, // Nacon
|
||||||
0x12ab, // Unknown
|
0x12ab, // Unknown
|
||||||
0x1430, // RedOctane
|
0x1430, // RedOctane
|
||||||
@@ -137,4 +139,17 @@ public class Xbox360Controller extends AbstractXboxController {
|
|||||||
// No need to fail init if the LED command fails
|
// No need to fail init if the LED command fails
|
||||||
return true;
|
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 com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class XboxOneController extends AbstractXboxController {
|
public class XboxOneController extends AbstractXboxController {
|
||||||
|
|
||||||
@@ -23,8 +24,31 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
0x24c6, // PowerA
|
0x24c6, // PowerA
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: odata_serial
|
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
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(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) {
|
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
super(device, connection, deviceId, listener);
|
super(device, connection, deviceId, listener);
|
||||||
@@ -111,13 +135,55 @@ public class XboxOneController extends AbstractXboxController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doInit() {
|
protected boolean doInit() {
|
||||||
// Send the initialization packet
|
// Send all applicable init packets
|
||||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
for (InitPacket pkt : INIT_PKTS) {
|
||||||
if (res != XB1_INIT_DATA.length) {
|
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
||||||
LimeLog.warning("Initialization transfer failed: "+res);
|
continue;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
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;
|
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_LEFT = 1;
|
||||||
int BUTTON_MIDDLE = 2;
|
int BUTTON_MIDDLE = 2;
|
||||||
int BUTTON_RIGHT = 3;
|
int BUTTON_RIGHT = 3;
|
||||||
|
int BUTTON_X1 = 4;
|
||||||
|
int BUTTON_X2 = 5;
|
||||||
|
|
||||||
void mouseMove(int deltaX, int deltaY);
|
void mouseMove(int deltaX, int deltaY);
|
||||||
void mouseButtonEvent(int buttonId, boolean down);
|
void mouseButtonEvent(int buttonId, boolean down);
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
// calculate new radius sizes depending
|
// calculate new radius sizes depending
|
||||||
radius_complete = getPercent(getCorrectWidth() / 2, 90);
|
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
||||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||||
|
|
||||||
@@ -338,6 +338,7 @@ public class AnalogStick extends VirtualControllerElement {
|
|||||||
} else {
|
} else {
|
||||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
notifyOnRevoke();
|
notifyOnRevoke();
|
||||||
|
|
||||||
// not longer pressed reset analog stick
|
// not longer pressed reset analog stick
|
||||||
notifyOnMovement(0, 0);
|
notifyOnMovement(0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-28
@@ -13,10 +13,13 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class VirtualController {
|
public class VirtualController {
|
||||||
public class ControllerInputContext {
|
public class ControllerInputContext {
|
||||||
@@ -36,12 +39,14 @@ public class VirtualController {
|
|||||||
|
|
||||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
private NvConnection connection = null;
|
private ControllerHandler controllerHandler;
|
||||||
private Context context = null;
|
private Context context = null;
|
||||||
|
|
||||||
private FrameLayout frame_layout = null;
|
private FrameLayout frame_layout = null;
|
||||||
private RelativeLayout relative_layout = null;
|
private RelativeLayout relative_layout = null;
|
||||||
|
|
||||||
|
private Timer retransmitTimer;
|
||||||
|
|
||||||
ControllerMode currentMode = ControllerMode.Active;
|
ControllerMode currentMode = ControllerMode.Active;
|
||||||
ControllerInputContext inputContext = new ControllerInputContext();
|
ControllerInputContext inputContext = new ControllerInputContext();
|
||||||
|
|
||||||
@@ -49,8 +54,8 @@ public class VirtualController {
|
|||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
|
|
||||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
||||||
this.connection = conn;
|
this.controllerHandler = controllerHandler;
|
||||||
this.frame_layout = layout;
|
this.frame_layout = layout;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
@@ -60,6 +65,7 @@ public class VirtualController {
|
|||||||
|
|
||||||
buttonConfigure = new Button(context);
|
buttonConfigure = new Button(context);
|
||||||
buttonConfigure.setAlpha(0.25f);
|
buttonConfigure.setAlpha(0.25f);
|
||||||
|
buttonConfigure.setFocusable(false);
|
||||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -86,6 +92,27 @@ 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() {
|
public void removeElements() {
|
||||||
for (VirtualControllerElement element : elements) {
|
for (VirtualControllerElement element : elements) {
|
||||||
relative_layout.removeView(element);
|
relative_layout.removeView(element);
|
||||||
@@ -140,32 +167,23 @@ public class VirtualController {
|
|||||||
return inputContext;
|
return inputContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendControllerInputContext() {
|
void sendControllerInputContext() {
|
||||||
sendControllerInputPacket();
|
_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() {
|
if (controllerHandler != null) {
|
||||||
try {
|
controllerHandler.reportOscState(
|
||||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
inputContext.inputMap,
|
||||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
inputContext.leftStickX,
|
||||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
inputContext.leftStickY,
|
||||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
inputContext.rightStickX,
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
inputContext.rightStickY,
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
inputContext.leftTrigger,
|
||||||
|
inputContext.rightTrigger
|
||||||
if (connection != null) {
|
);
|
||||||
connection.sendControllerInput(
|
|
||||||
inputContext.inputMap,
|
|
||||||
inputContext.leftTrigger,
|
|
||||||
inputContext.rightTrigger,
|
|
||||||
inputContext.leftStickX,
|
|
||||||
inputContext.leftStickY,
|
|
||||||
inputContext.rightStickX,
|
|
||||||
inputContext.rightStickY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-46
@@ -166,58 +166,54 @@ public abstract class VirtualControllerElement extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void showConfigurationDialog() {
|
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[]{
|
CharSequence functions[] = new CharSequence[]{
|
||||||
"Move",
|
"Move",
|
||||||
"Resize",
|
"Resize",
|
||||||
/*election
|
/*election
|
||||||
"Set n
|
"Set n
|
||||||
Disable color sormal color",
|
Disable color sormal color",
|
||||||
"Set pressed 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"
|
default: { // cancel
|
||||||
};
|
actionCancel();
|
||||||
|
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
case 3: { // set pressed color
|
|
||||||
actionShowPressedColorChooser();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
default: { // cancel
|
|
||||||
actionCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
AlertDialog alert = alertBuilder.create();
|
});
|
||||||
// show menu
|
AlertDialog alert = alertBuilder.create();
|
||||||
alert.show();
|
// show menu
|
||||||
} catch (Exception e) {
|
alert.show();
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.jcodec.codecs.h264.H264Utils;
|
import org.jcodec.codecs.h264.H264Utils;
|
||||||
@@ -9,10 +8,12 @@ import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
|||||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.jni.MoonBridge;
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
@@ -39,9 +40,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private boolean submittedCsd;
|
private boolean submittedCsd;
|
||||||
private boolean submitCsdNextCall;
|
private boolean submitCsdNextCall;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private Thread[] spinnerThreads;
|
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private boolean adaptivePlayback, directSubmit;
|
private boolean adaptivePlayback, directSubmit;
|
||||||
private boolean constrainedHighProfile;
|
private boolean constrainedHighProfile;
|
||||||
@@ -57,6 +58,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private String glRenderer;
|
private String glRenderer;
|
||||||
private boolean foreground = true;
|
private boolean foreground = true;
|
||||||
private boolean legacyFrameDropRendering = false;
|
private boolean legacyFrameDropRendering = false;
|
||||||
|
private PerfOverlayListener perfListener;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
private SeqParameterSet savedSps;
|
private SeqParameterSet savedSps;
|
||||||
@@ -65,13 +67,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
private long initialExceptionTimestamp;
|
private long initialExceptionTimestamp;
|
||||||
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
|
||||||
|
|
||||||
|
private VideoStats activeWindowVideoStats;
|
||||||
|
private VideoStats lastWindowVideoStats;
|
||||||
|
private VideoStats globalVideoStats;
|
||||||
|
|
||||||
private long lastTimestampUs;
|
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 lastFrameNumber;
|
||||||
private int refreshRate;
|
private int refreshRate;
|
||||||
private PreferenceConfiguration prefs;
|
private PreferenceConfiguration prefs;
|
||||||
@@ -121,24 +121,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
this.renderTarget = renderTarget;
|
this.renderTarget = renderTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
|
public MediaCodecDecoderRenderer(Context context, PreferenceConfiguration prefs,
|
||||||
CrashListener crashListener, int consecutiveCrashCount,
|
CrashListener crashListener, int consecutiveCrashCount,
|
||||||
boolean meteredData, boolean requestedHdr,
|
boolean meteredData, boolean requestedHdr,
|
||||||
String glRenderer) {
|
String glRenderer, PerfOverlayListener perfListener) {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
this.prefs = prefs;
|
this.prefs = prefs;
|
||||||
this.crashListener = crashListener;
|
this.crashListener = crashListener;
|
||||||
this.consecutiveCrashCount = consecutiveCrashCount;
|
this.consecutiveCrashCount = consecutiveCrashCount;
|
||||||
this.glRenderer = glRenderer;
|
this.glRenderer = glRenderer;
|
||||||
|
this.perfListener = perfListener;
|
||||||
|
|
||||||
// Disable spinner threads in battery saver mode or 4K
|
this.activeWindowVideoStats = new VideoStats();
|
||||||
if (prefs.batterySaver || prefs.height >= 2160) {
|
this.lastWindowVideoStats = new VideoStats();
|
||||||
spinnerThreads = new Thread[0];
|
this.globalVideoStats = new VideoStats();
|
||||||
}
|
|
||||||
else {
|
|
||||||
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
|
|
||||||
}
|
|
||||||
|
|
||||||
avcDecoder = findAvcDecoder();
|
avcDecoder = findAvcDecoder();
|
||||||
if (avcDecoder != null) {
|
if (avcDecoder != null) {
|
||||||
@@ -217,15 +215,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void notifyVideoForeground() {
|
public void notifyVideoForeground() {
|
||||||
startSpinnerThreads();
|
|
||||||
foreground = true;
|
foreground = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyVideoBackground() {
|
public void notifyVideoBackground() {
|
||||||
// Signal the spinner threads to stop but
|
|
||||||
// don't wait for them to terminate to avoid
|
|
||||||
// delaying the state transition
|
|
||||||
signalSpinnerStop();
|
|
||||||
foreground = false;
|
foreground = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +319,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
||||||
if (delta >= 0 && delta < 1000) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
if (USE_FRAME_RENDER_TIME) {
|
if (USE_FRAME_RENDER_TIME) {
|
||||||
totalTimeMs += delta;
|
activeWindowVideoStats.totalTimeMs += delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,23 +413,30 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the last buffer
|
// Render the last buffer
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// Use a PTS that will cause this frame to never be dropped if frame dropping
|
if (legacyFrameDropRendering) {
|
||||||
// is disabled
|
// Use a PTS that will cause this frame to be dropped if another comes in within
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, 0);
|
// 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 {
|
else {
|
||||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalFramesRendered++;
|
activeWindowVideoStats.totalFramesRendered++;
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||||
if (delta >= 0 && delta < 1000) {
|
if (delta >= 0 && delta < 1000) {
|
||||||
decoderTimeMs += delta;
|
activeWindowVideoStats.decoderTimeMs += delta;
|
||||||
if (!USE_FRAME_RENDER_TIME) {
|
if (!USE_FRAME_RENDER_TIME) {
|
||||||
totalTimeMs += delta;
|
activeWindowVideoStats.totalTimeMs += delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -462,73 +462,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
rendererThread.start();
|
rendererThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startSpinnerThread(final int i) {
|
|
||||||
spinnerThreads[i] = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// This thread exists to keep the CPU at a higher DVFS state on devices
|
|
||||||
// where the governor scales clock speed sporadically, causing dropped frames.
|
|
||||||
//
|
|
||||||
// Run until we notice our thread has been removed from the spinner threads
|
|
||||||
// array. Even if we don't notice immediately, we'll notice soon enough.
|
|
||||||
// This will also ensure we terminate even if someone has restarted spinning
|
|
||||||
// before we realized we should stop.
|
|
||||||
while (this == spinnerThreads[i]) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(0, 1);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spinnerThreads[i].setName("Spinner-"+i);
|
|
||||||
spinnerThreads[i].setPriority(Thread.MIN_PRIORITY);
|
|
||||||
spinnerThreads[i].start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startSpinnerThreads() {
|
|
||||||
LimeLog.info("Using "+spinnerThreads.length+" spinner threads");
|
|
||||||
for (int i = 0; i < spinnerThreads.length; i++) {
|
|
||||||
if (spinnerThreads[i] != null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
startSpinnerThread(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Thread[] signalSpinnerStop() {
|
|
||||||
// Capture current running threads
|
|
||||||
Thread[] runningThreads = Arrays.copyOf(spinnerThreads, spinnerThreads.length);
|
|
||||||
|
|
||||||
// Clear the spinner threads to signal their termination
|
|
||||||
for (int i = 0; i < spinnerThreads.length; i++) {
|
|
||||||
spinnerThreads[i] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt the threads
|
|
||||||
for (int i = 0; i < runningThreads.length; i++) {
|
|
||||||
if (runningThreads[i] != null) {
|
|
||||||
runningThreads[i].interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return runningThreads;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopSpinnerThreads() {
|
|
||||||
// Signal and wait for the threads to stop
|
|
||||||
Thread[] runningThreads = signalSpinnerStop();
|
|
||||||
for (int i = 0; i < runningThreads.length; i++) {
|
|
||||||
if (runningThreads[i] != null) {
|
|
||||||
try {
|
|
||||||
runningThreads[i].join();
|
|
||||||
} catch (InterruptedException ignored) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int dequeueInputBuffer() {
|
private int dequeueInputBuffer() {
|
||||||
int index = -1;
|
int index = -1;
|
||||||
long startTime;
|
long startTime;
|
||||||
@@ -570,7 +503,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start() {
|
||||||
startRendererThread();
|
startRendererThread();
|
||||||
startSpinnerThreads();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// !!! May be called even if setup()/start() fails !!!
|
// !!! May be called even if setup()/start() fails !!!
|
||||||
@@ -593,9 +525,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
try {
|
try {
|
||||||
rendererThread.join();
|
rendererThread.join();
|
||||||
} catch (InterruptedException ignored) { }
|
} catch (InterruptedException ignored) { }
|
||||||
|
|
||||||
// Halt the spinner threads
|
|
||||||
stopSpinnerThreads();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -664,17 +593,58 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
return MoonBridge.DR_OK;
|
return MoonBridge.DR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalFramesReceived++;
|
if (lastFrameNumber == 0) {
|
||||||
|
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
|
||||||
// We can receive the same "frame" multiple times if it's an IDR frame.
|
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
||||||
// In that case, each frame start NALU is submitted independently.
|
// We can receive the same "frame" multiple times if it's an IDR frame.
|
||||||
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
|
// In that case, each frame start NALU is submitted independently.
|
||||||
framesLost += frameNumber - lastFrameNumber - 1;
|
activeWindowVideoStats.framesLost += frameNumber - lastFrameNumber - 1;
|
||||||
frameLossEvents++;
|
activeWindowVideoStats.totalFrames += frameNumber - lastFrameNumber - 1;
|
||||||
|
activeWindowVideoStats.frameLossEvents++;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastFrameNumber = frameNumber;
|
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;
|
int inputBufferIndex;
|
||||||
ByteBuffer buf;
|
ByteBuffer buf;
|
||||||
|
|
||||||
@@ -682,7 +652,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
|
|
||||||
if (!FRAME_RENDER_TIME_ONLY) {
|
if (!FRAME_RENDER_TIME_ONLY) {
|
||||||
// Count time from first packet received to decode start
|
// Count time from first packet received to decode start
|
||||||
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestampUs <= lastTimestampUs) {
|
if (timestampUs <= lastTimestampUs) {
|
||||||
@@ -718,7 +688,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
LimeLog.info("Patching level_idc to 31");
|
LimeLog.info("Patching level_idc to 31");
|
||||||
sps.levelIdc = 31;
|
sps.levelIdc = 31;
|
||||||
}
|
}
|
||||||
if (initialWidth <= 1280 && initialHeight <= 720) {
|
else if (initialWidth <= 1280 && initialHeight <= 720) {
|
||||||
// Max 5 buffered frames at 1280x720x60
|
// Max 5 buffered frames at 1280x720x60
|
||||||
LimeLog.info("Patching level_idc to 32");
|
LimeLog.info("Patching level_idc to 32");
|
||||||
sps.levelIdc = 32;
|
sps.levelIdc = 32;
|
||||||
@@ -989,17 +959,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getAverageEndToEndLatency() {
|
public int getAverageEndToEndLatency() {
|
||||||
if (totalFramesReceived == 0) {
|
if (globalVideoStats.totalFramesReceived == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (int)(totalTimeMs / totalFramesReceived);
|
return (int)(globalVideoStats.totalTimeMs / globalVideoStats.totalFramesReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAverageDecoderLatency() {
|
public int getAverageDecoderLatency() {
|
||||||
if (totalFramesReceived == 0) {
|
if (globalVideoStats.totalFramesReceived == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (int)(decoderTimeMs / totalFramesReceived);
|
return (int)(globalVideoStats.decoderTimeMs / globalVideoStats.totalFramesReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class DecoderHungException extends RuntimeException {
|
static class DecoderHungException extends RuntimeException {
|
||||||
@@ -1060,9 +1030,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
|
|||||||
str += "FPS target: "+renderer.refreshRate+"\n";
|
str += "FPS target: "+renderer.refreshRate+"\n";
|
||||||
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
|
||||||
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
|
||||||
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
|
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
|
||||||
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
|
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
|
||||||
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\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 end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
|
||||||
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ public class MediaCodecHelper {
|
|||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
// Blacklist software decoders that don't support H264 high profile,
|
||||||
// but exclude the official AOSP emulator from this restriction.
|
// but exclude the official AOSP and CrOS emulator from this restriction.
|
||||||
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
|
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
}
|
}
|
||||||
@@ -131,10 +131,11 @@ public class MediaCodecHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||||
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
|
||||||
// to actually be tested. Ugh...
|
// to actually be tested.
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
}
|
}
|
||||||
|
|
||||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||||
@@ -159,12 +160,16 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
// We see a bunch of crashes on MediaTek Android TVs running
|
// We see a bunch of crashes on MediaTek Android TVs running
|
||||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
||||||
// these devices and hope they fix it in Oreo.
|
// these devices and hope they fix it in Pie.
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isPowerVR(String glRenderer) {
|
||||||
|
return glRenderer.toLowerCase().contains("powervr");
|
||||||
|
}
|
||||||
|
|
||||||
private static String getAdrenoVersionString(String glRenderer) {
|
private static String getAdrenoVersionString(String glRenderer) {
|
||||||
glRenderer = glRenderer.toLowerCase().trim();
|
glRenderer = glRenderer.toLowerCase().trim();
|
||||||
|
|
||||||
@@ -258,6 +263,18 @@ public class MediaCodecHelper {
|
|||||||
else {
|
else {
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
||||||
|
// PowerVR GPUs have good HEVC support.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
|
||||||
|
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
|
||||||
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
|
||||||
|
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
|
||||||
|
// required to make it work adds a huge amount of latency.
|
||||||
|
LimeLog.info("Added omx.mtk to RFI list for HEVC");
|
||||||
|
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
@@ -336,7 +353,7 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
// This device seems to crash constantly at 720p, so try disabling
|
// This device seems to crash constantly at 720p, so try disabling
|
||||||
// RFI to see if we can get that under control.
|
// RFI to see if we can get that under control.
|
||||||
if (Build.PRODUCT.equalsIgnoreCase("b3_att_us")) {
|
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +474,24 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
|
||||||
|
// Use the new isSoftwareOnly() function on Android Q
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
if (codecInfo.isSoftwareOnly()) {
|
||||||
|
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for explicitly blacklisted decoders
|
||||||
|
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
||||||
|
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static MediaCodecInfo findFirstDecoder(String mimeType) {
|
public static MediaCodecInfo findFirstDecoder(String mimeType) {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
@@ -465,15 +500,14 @@ public class MediaCodecHelper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for explicitly blacklisted decoders
|
|
||||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
|
||||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports the specified video format
|
// Find a decoder that supports the specified video format
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
if (mime.equalsIgnoreCase(mimeType)) {
|
||||||
|
// Skip blacklisted codecs
|
||||||
|
if (isCodecBlacklisted(codecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
||||||
return codecInfo;
|
return codecInfo;
|
||||||
}
|
}
|
||||||
@@ -513,17 +547,16 @@ public class MediaCodecHelper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for explicitly blacklisted decoders
|
|
||||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
|
||||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a decoder that supports the requested video format
|
// Find a decoder that supports the requested video format
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
if (mime.equalsIgnoreCase(mimeType)) {
|
||||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
||||||
|
|
||||||
|
// Skip blacklisted codecs
|
||||||
|
if (isCodecBlacklisted(codecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||||
|
|
||||||
if (requiredProfile != -1) {
|
if (requiredProfile != -1) {
|
||||||
|
|||||||
@@ -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;
|
package com.limelight.computers;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.net.UnknownHostException;
|
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.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@@ -17,15 +18,15 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
public class ComputerDatabaseManager {
|
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_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 COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
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;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
@@ -38,86 +39,98 @@ public class ComputerDatabaseManager {
|
|||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
}
|
}
|
||||||
initializeDb();
|
initializeDb(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
computerDb.close();
|
computerDb.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeDb() {
|
private void initializeDb(Context c) {
|
||||||
// Create tables if they aren't already there
|
// Create tables if they aren't already there
|
||||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
computerDb.execSQL(String.format((Locale)null,
|
||||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT 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_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||||
REMOTE_IP_COLUMN_NAME, MAC_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) {
|
public void deleteComputer(ComputerDetails details) {
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
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);
|
StringBuilder addresses = new StringBuilder();
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
addresses.append(details.localAddress != null ? details.localAddress : "");
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
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);
|
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) {
|
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
details.name = c.getString(0);
|
details.uuid = c.getString(0);
|
||||||
|
details.name = c.getString(1);
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// An earlier schema defined addresses as byte blobs. We'll
|
details.localAddress = readNonEmptyString(addresses[0]);
|
||||||
// gracefully migrate those to strings so we can store DNS names
|
details.remoteAddress = readNonEmptyString(addresses[1]);
|
||||||
// too. To disambiguate, we'll need to prefix them with a string
|
details.manualAddress = readNonEmptyString(addresses[2]);
|
||||||
// greater than the allowable IP address length.
|
details.ipv6Address = readNonEmptyString(addresses[3]);
|
||||||
try {
|
|
||||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
details.macAddress = c.getString(3);
|
||||||
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 {
|
try {
|
||||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
byte[] derCertData = c.getBlob(4);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
@@ -126,16 +139,7 @@ public class ComputerDatabaseManager {
|
|||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
computerList.add(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
@@ -143,8 +147,8 @@ public class ComputerDatabaseManager {
|
|||||||
return computerList;
|
return computerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByUUID(String uuid) {
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
c.close();
|
c.close();
|
||||||
@@ -154,13 +158,6 @@ public class ComputerDatabaseManager {
|
|||||||
ComputerDetails details = getComputerFromCursor(c);
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
c.close();
|
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;
|
return details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,22 @@ package com.limelight.computers;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
import java.net.Inet4Address;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.UnknownHostException;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.discovery.DiscoveryService;
|
import com.limelight.discovery.DiscoveryService;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
@@ -95,7 +96,6 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -106,17 +106,27 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
// If it's online, update our persistent state
|
// If it's online, update our persistent state
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
if (!newPc) {
|
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
||||||
// Check if it's in the database because it could have been
|
|
||||||
// removed after this was issued
|
// Check if it's in the database because it could have been
|
||||||
if (dbManager.getComputerByName(details.name) == null) {
|
// removed after this was issued
|
||||||
// It's gone
|
if (!newPc && existingComputer == null) {
|
||||||
releaseLocalDatabaseReference();
|
// It's gone
|
||||||
return false;
|
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
|
// Don't call the listener if this is a failed lookup of a new PC
|
||||||
@@ -156,7 +166,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for " + tuple.computer.localAddress);
|
t.setName("Polling thread for " + tuple.computer.name);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +187,6 @@ public class ComputerManagerService extends Service {
|
|||||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report this computer initially
|
// Report this computer initially
|
||||||
@@ -212,12 +221,12 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(ComputerDetails computer) {
|
||||||
ComputerManagerService.this.removeComputer(name);
|
ComputerManagerService.this.removeComputer(computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopPolling() {
|
public void stopPolling() {
|
||||||
@@ -233,7 +242,7 @@ public class ComputerManagerService extends Service {
|
|||||||
return idManager.getUniqueId();
|
return idManager.getUniqueId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputer(UUID uuid) {
|
public ComputerDetails getComputer(String uuid) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
if (uuid.equals(tuple.computer.uuid)) {
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
@@ -245,7 +254,7 @@ public class ComputerManagerService extends Service {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void invalidateStateForComputer(UUID uuid) {
|
public void invalidateStateForComputer(String uuid) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
if (uuid.equals(tuple.computer.uuid)) {
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
@@ -253,7 +262,6 @@ public class ComputerManagerService extends Service {
|
|||||||
// from wiping this change out
|
// from wiping this change out
|
||||||
synchronized (tuple.networkLock) {
|
synchronized (tuple.networkLock) {
|
||||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,8 +298,27 @@ public class ComputerManagerService extends Service {
|
|||||||
return new MdnsDiscoveryListener() {
|
return new MdnsDiscoveryListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
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) {
|
||||||
|
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (computer.getIpv6Address() != null) {
|
||||||
|
details.ipv6Address = computer.getIpv6Address().getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
// Kick off a serverinfo poll on this machine
|
// 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
|
@Override
|
||||||
@@ -307,22 +334,13 @@ public class ComputerManagerService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
|
private void addTuple(ComputerDetails details) {
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
// Check if this is the same computer
|
// Check if this is the same computer
|
||||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||||
if (manuallyAdded) {
|
// Update the saved computer with potentially new details
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
tuple.computer.update(details);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a polling thread if polling is active
|
// Start a polling thread if polling is active
|
||||||
if (pollingActive && tuple.thread == null) {
|
if (pollingActive && tuple.thread == null) {
|
||||||
@@ -347,15 +365,26 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
|
||||||
// Setup a placeholder
|
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
|
||||||
fakeDetails.localAddress = addr;
|
|
||||||
fakeDetails.remoteAddress = addr;
|
|
||||||
|
|
||||||
// Block while we try to fill the details
|
// Block while we try to fill the details
|
||||||
try {
|
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) {
|
} catch (InterruptedException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -365,30 +394,26 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
|
||||||
|
|
||||||
// Start a polling thread for this machine
|
// Start a polling thread for this machine
|
||||||
addTuple(fakeDetails, manuallyAdded);
|
addTuple(fakeDetails);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!manuallyAdded) {
|
|
||||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(ComputerDetails computer) {
|
||||||
if (!getLocalDatabaseReference()) {
|
if (!getLocalDatabaseReference()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove it from the database
|
// Remove it from the database
|
||||||
dbManager.deleteComputer(name);
|
dbManager.deleteComputer(computer);
|
||||||
|
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
// Remove the computer from the computer list
|
// Remove the computer from the computer list
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
if (tuple.computer.name.equals(name)) {
|
if (tuple.computer.uuid.equals(computer.uuid)) {
|
||||||
if (tuple.thread != null) {
|
if (tuple.thread != null) {
|
||||||
// Interrupt the thread on this entry
|
// Interrupt the thread on this entry
|
||||||
tuple.thread.interrupt();
|
tuple.thread.interrupt();
|
||||||
@@ -425,21 +450,29 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
ComputerDetails newDetails = http.getComputerDetails();
|
ComputerDetails newDetails = http.getComputerDetails();
|
||||||
|
|
||||||
// Check if this is the PC we expected
|
// Check if this is the PC we expected
|
||||||
if (details.uuid != null && newDetails.uuid != null &&
|
if (newDetails.uuid == null) {
|
||||||
!details.uuid.equals(newDetails.uuid)) {
|
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!
|
// We got the wrong PC!
|
||||||
LimeLog.info("Polling returned the wrong PC!");
|
LimeLog.info("Polling returned the wrong PC!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the new active address
|
||||||
|
newDetails.activeAddress = address;
|
||||||
|
|
||||||
return newDetails;
|
return newDetails;
|
||||||
} catch (Exception e) {
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +480,11 @@ public class ComputerManagerService extends Service {
|
|||||||
// Just try to establish a TCP connection to speculatively detect a running
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
// GFE server
|
// GFE server
|
||||||
private boolean fastPollIp(String address) {
|
private boolean fastPollIp(String address) {
|
||||||
|
if (address == null) {
|
||||||
|
// Don't bother if our address is null
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Socket s = new Socket();
|
Socket s = new Socket();
|
||||||
try {
|
try {
|
||||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
@@ -475,12 +513,16 @@ public class ComputerManagerService extends Service {
|
|||||||
t.start();
|
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[] remoteInfo = new boolean[2];
|
||||||
final boolean[] localInfo = 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(localAddress, localInfo);
|
||||||
startFastPollThread(remoteAddress, remoteInfo);
|
startFastPollThread(remoteAddress, remoteInfo);
|
||||||
|
startFastPollThread(manualAddress, manualInfo);
|
||||||
|
startFastPollThread(ipv6Address, ipv6Info);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@@ -489,174 +531,90 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (localInfo[1]) {
|
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) {
|
synchronized (remoteInfo) {
|
||||||
while (!remoteInfo[0]) {
|
while (!remoteInfo[0]) {
|
||||||
remoteInfo.wait(500);
|
remoteInfo.wait(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteInfo[1]) {
|
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) {
|
if (ipv6Info[1]) {
|
||||||
try {
|
return ipv6Address;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
return null;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
ComputerDetails polledDetails;
|
||||||
if (initialReachTuple == null) {
|
|
||||||
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
// Try using the active address from fast-poll
|
||||||
// Neither IP address reported in the serverinfo response was the one we used.
|
polledDetails = tryPollIp(details, candidateAddress);
|
||||||
// Poll again to see if we can contact this machine on either of its reported addresses.
|
if (polledDetails == null) {
|
||||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
// If that failed, try all unique addresses except what we've
|
||||||
if (confirmationReachTuple == null) {
|
// already tried
|
||||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||||
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
|
uniqueAddresses.add(details.localAddress);
|
||||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
uniqueAddresses.add(details.manualAddress);
|
||||||
}
|
uniqueAddresses.add(details.remoteAddress);
|
||||||
else {
|
uniqueAddresses.add(details.ipv6Address);
|
||||||
// We got it on one of the returned addresses; replace the original reach tuple
|
for (String addr : uniqueAddresses) {
|
||||||
// with the new one
|
if (addr == null || addr.equals(candidateAddress)) {
|
||||||
initialReachTuple = confirmationReachTuple;
|
continue;
|
||||||
|
}
|
||||||
|
polledDetails = tryPollIp(details, addr);
|
||||||
|
if (polledDetails != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save some details about the old state of the PC that we may wish
|
if (polledDetails != null) {
|
||||||
// to restore later.
|
details.update(polledDetails);
|
||||||
String savedMacAddress = details.macAddress;
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
// We never want to lose IP addresses by polling server info. If we get a poll back
|
return false;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -679,7 +637,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
for (ComputerDetails computer : dbManager.getAllComputers()) {
|
||||||
// Add tuples for each computer
|
// Add tuples for each computer
|
||||||
addTuple(computer, true);
|
addTuple(computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -757,8 +715,9 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
int emptyAppListResponses = 0;
|
int emptyAppListResponses = 0;
|
||||||
do {
|
do {
|
||||||
// Can't poll if it's not online
|
// Can't poll if it's not online or paired
|
||||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
if (computer.state != ComputerDetails.State.ONLINE ||
|
||||||
|
computer.pairState != PairingManager.PairState.PAIRED) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
@@ -774,7 +733,7 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
String appList;
|
String appList;
|
||||||
if (tuple != null) {
|
if (tuple != null) {
|
||||||
@@ -798,12 +757,12 @@ public class ComputerManagerService extends Service {
|
|||||||
// in a row, we'll go ahead and believe it.
|
// in a row, we'll go ahead and believe it.
|
||||||
emptyAppListResponses++;
|
emptyAppListResponses++;
|
||||||
}
|
}
|
||||||
if (appList != null && !appList.isEmpty() &&
|
if (!appList.isEmpty() &&
|
||||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||||
// Open the cache file
|
// Open the cache file
|
||||||
OutputStream cacheOut = null;
|
OutputStream cacheOut = null;
|
||||||
try {
|
try {
|
||||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
||||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -830,7 +789,7 @@ public class ComputerManagerService extends Service {
|
|||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (appList == null || appList.isEmpty()) {
|
else if (appList.isEmpty()) {
|
||||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -841,7 +800,7 @@ public class ComputerManagerService extends Service {
|
|||||||
} while (waitPollingDelay());
|
} while (waitPollingDelay());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
thread.setName("App list polling thread for " + computer.localAddress);
|
thread.setName("App list polling thread for " + computer.name);
|
||||||
thread.start();
|
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,7 +1,6 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -46,13 +45,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
}
|
}
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = (int) scalingDivisor;
|
|
||||||
|
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
new MemoryAssetLoader(),
|
new MemoryAssetLoader(),
|
||||||
new DiskAssetLoader(context.getCacheDir()));
|
new DiskAssetLoader(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public void cancelQueuedOperations() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.widget.TextView;
|
|||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -46,7 +47,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
imgView.setAlpha(0.4f);
|
imgView.setAlpha(0.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
|
||||||
prgView.setVisibility(View.VISIBLE);
|
prgView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -77,6 +78,14 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
overlayView.setAlpha(0.4f);
|
overlayView.setAlpha(0.4f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// 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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.limelight.grid.assets;
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.ImageDecoder;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
@@ -19,14 +23,23 @@ public class DiskAssetLoader {
|
|||||||
private static final int STANDARD_ASSET_WIDTH = 300;
|
private static final int STANDARD_ASSET_WIDTH = 300;
|
||||||
private static final int STANDARD_ASSET_HEIGHT = 400;
|
private static final int STANDARD_ASSET_HEIGHT = 400;
|
||||||
|
|
||||||
|
private final boolean isLowRamDevice;
|
||||||
private final File cacheDir;
|
private final File cacheDir;
|
||||||
|
|
||||||
public DiskAssetLoader(File cacheDir) {
|
public DiskAssetLoader(Context context) {
|
||||||
this.cacheDir = cacheDir;
|
this.cacheDir = context.getCacheDir();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
this.isLowRamDevice =
|
||||||
|
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use conservative low RAM behavior on very old devices
|
||||||
|
this.isLowRamDevice = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
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
|
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
||||||
@@ -52,7 +65,7 @@ public class DiskAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
// Don't bother with anything if it doesn't exist
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@@ -66,38 +79,79 @@ public class DiskAssetLoader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup bounds of the downloaded image
|
Bitmap bmp;
|
||||||
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
|
||||||
decodeOnlyOptions.inJustDecodeBounds = true;
|
// For OSes prior to P, we have to use the ugly BitmapFactory API
|
||||||
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
|
// Lookup bounds of the downloaded image
|
||||||
// Dimensions set to -1 on error. Return value always null.
|
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
||||||
return null;
|
decodeOnlyOptions.inJustDecodeBounds = true;
|
||||||
|
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
|
||||||
|
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
|
||||||
|
// Dimensions set to -1 on error. Return value always null.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
|
||||||
|
|
||||||
|
// Load the image scaled to the appropriate size
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
|
||||||
|
STANDARD_ASSET_WIDTH / sampleSize,
|
||||||
|
STANDARD_ASSET_HEIGHT / sampleSize);
|
||||||
|
if (isLowRamDevice) {
|
||||||
|
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||||
|
options.inDither = true;
|
||||||
|
}
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
options.inPreferredConfig = Bitmap.Config.HARDWARE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
|
// On P, we can get a bitmap back in one step with ImageDecoder
|
||||||
|
try {
|
||||||
// Load the image scaled to the appropriate size
|
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
@Override
|
||||||
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
|
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
||||||
STANDARD_ASSET_WIDTH / sampleSize,
|
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
||||||
STANDARD_ASSET_HEIGHT / sampleSize);
|
if (isLowRamDevice) {
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
||||||
options.inDither = true;
|
}
|
||||||
Bitmap bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
}
|
||||||
|
});
|
||||||
if (bmp != null) {
|
} catch (IOException e) {
|
||||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bmp;
|
return bmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
OutputStream out = null;
|
OutputStream out = null;
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
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);
|
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||||
success = true;
|
success = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -111,7 +165,7 @@ public class DiskAssetLoader {
|
|||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class MemoryAssetLoader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
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) {
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ public class NetworkAssetLoader {
|
|||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
try {
|
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);
|
in = http.getBoxArt(tuple.app);
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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 int negotiatedFps;
|
||||||
|
public boolean negotiatedHdr;
|
||||||
|
|
||||||
|
public int videoCapabilities;
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
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;
|
||||||
|
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Take what the client wanted
|
||||||
|
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||||
|
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||||
|
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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 (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.negotiatedFps, context.streamConfig.getBitrate(),
|
||||||
|
context.streamConfig.getMaxPacketSize(),
|
||||||
|
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
|
||||||
|
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 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 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, long errorCode);
|
||||||
|
|
||||||
|
void connectionStarted();
|
||||||
|
void connectionTerminated(long errorCode);
|
||||||
|
void connectionStatusUpdate(int connectionStatus);
|
||||||
|
|
||||||
|
void displayMessage(String message);
|
||||||
|
void displayTransientMessage(String message);
|
||||||
|
|
||||||
|
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
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 static final int CHANNEL_COUNT_STEREO = 2;
|
||||||
|
private static final int CHANNEL_COUNT_5_1 = 6;
|
||||||
|
|
||||||
|
private static final int CHANNEL_MASK_STEREO = 0x3;
|
||||||
|
private static final int CHANNEL_MASK_5_1 = 0xFC;
|
||||||
|
|
||||||
|
private NvApp app;
|
||||||
|
private int width, height;
|
||||||
|
private int refreshRate;
|
||||||
|
private int clientRefreshRateX100;
|
||||||
|
private int bitrate;
|
||||||
|
private boolean sops;
|
||||||
|
private boolean enableAdaptiveResolution;
|
||||||
|
private boolean playLocalAudio;
|
||||||
|
private int maxPacketSize;
|
||||||
|
private int remote;
|
||||||
|
private int audioChannelMask;
|
||||||
|
private int audioChannelCount;
|
||||||
|
private int 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 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(int audioConfig) {
|
||||||
|
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
|
||||||
|
config.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||||
|
config.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||||
|
}
|
||||||
|
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
|
||||||
|
config.audioChannelCount = CHANNEL_COUNT_5_1;
|
||||||
|
config.audioChannelMask = CHANNEL_MASK_5_1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException("Invalid audio configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
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.bitrate = 10000;
|
||||||
|
this.maxPacketSize = 1024;
|
||||||
|
this.remote = STREAM_CFG_AUTO;
|
||||||
|
this.sops = true;
|
||||||
|
this.enableAdaptiveResolution = false;
|
||||||
|
this.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||||
|
this.audioChannelMask = CHANNEL_MASK_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 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 int getAudioChannelCount() {
|
||||||
|
return audioChannelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAudioChannelMask() {
|
||||||
|
return audioChannelMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int 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,13 @@
|
|||||||
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
public interface AudioRenderer {
|
||||||
|
int setup(int audioConfiguration);
|
||||||
|
|
||||||
|
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("State: ").append(state).append("\n");
|
||||||
|
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||||
|
str.append("Name: ").append(name).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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,686 @@
|
|||||||
|
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 {
|
||||||
|
int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
|
||||||
|
if (statusCode != 200) {
|
||||||
|
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 IOException("HTTP request failed: "+response.code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 60 when starting. This won't impact our ability
|
||||||
|
// to get > 60 FPS while actually streaming though.
|
||||||
|
int fps = context.negotiatedFps > 60 ? 60 : context.negotiatedFps;
|
||||||
|
|
||||||
|
// 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.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()) +
|
||||||
|
(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.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()),
|
||||||
|
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,340 @@
|
|||||||
|
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.
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
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 int AUDIO_CONFIGURATION_STEREO = 0;
|
||||||
|
public static final int AUDIO_CONFIGURATION_51_SURROUND = 1;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 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) {
|
||||||
|
if (audioRenderer != null) {
|
||||||
|
return audioRenderer.setup(audioConfiguration);
|
||||||
|
}
|
||||||
|
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, long errorCode) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.stageFailed(getStageName(stage), errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClConnectionStarted() {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.connectionStarted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClConnectionTerminated(long 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 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 String getStageName(int stage);
|
||||||
|
|
||||||
|
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
||||||
|
|
||||||
|
public static native int getPendingAudioFrames();
|
||||||
|
|
||||||
|
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;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.Inet4Address;
|
import java.net.Inet4Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InterfaceAddress;
|
import java.net.InterfaceAddress;
|
||||||
@@ -9,8 +10,11 @@ import java.net.UnknownHostException;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.R;
|
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.Dialog;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
@@ -24,6 +28,7 @@ import android.content.ServiceConnection;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -90,14 +95,28 @@ public class AddComputerManually extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
|
||||||
boolean wrongSiteLocal = false;
|
boolean wrongSiteLocal = false;
|
||||||
boolean success;
|
boolean success;
|
||||||
|
|
||||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
getResources().getString(R.string.msg_add_pc), false);
|
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){
|
if (!success){
|
||||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||||
}
|
}
|
||||||
@@ -196,12 +215,7 @@ public class AddComputerManually extends Activity {
|
|||||||
(keyEvent != null &&
|
(keyEvent != null &&
|
||||||
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||||
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||||
if (hostText.getText().length() == 0) {
|
return handleDoneEvent();
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
computersToAdd.add(hostText.getText().toString().trim());
|
|
||||||
}
|
}
|
||||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||||
// This is how the Fire TV dismisses the keyboard
|
// This is how the Fire TV dismisses the keyboard
|
||||||
@@ -214,8 +228,28 @@ public class AddComputerManually extends Activity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
handleDoneEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
// Bind to the ComputerManager service
|
||||||
bindService(new Intent(AddComputerManually.this,
|
bindService(new Intent(AddComputerManually.this,
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if the event should be eaten
|
||||||
|
private boolean handleDoneEvent() {
|
||||||
|
String hostAddress = hostText.getText().toString().trim();
|
||||||
|
|
||||||
|
if (hostAddress.length() == 0) {
|
||||||
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
computersToAdd.add(hostAddress);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.limelight.preferences;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.DialogPreference;
|
import android.preference.DialogPreference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import android.os.Build;
|
|||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||||
|
|
||||||
|
|
||||||
|
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";
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
||||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
@@ -24,24 +28,19 @@ public class PreferenceConfiguration {
|
|||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||||
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
||||||
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
|
|
||||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
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_HDR_PREF_STRING = "checkbox_enable_hdr";
|
||||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
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 BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
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 int BITRATE_DEFAULT_360_30 = 1000;
|
static final String DEFAULT_RESOLUTION = "720p";
|
||||||
private static final int BITRATE_DEFAULT_360_60 = 2000;
|
static final String DEFAULT_FPS = "60";
|
||||||
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;
|
|
||||||
private static final boolean DEFAULT_STRETCH = false;
|
private static final boolean DEFAULT_STRETCH = false;
|
||||||
private static final boolean DEFAULT_SOPS = true;
|
private static final boolean DEFAULT_SOPS = true;
|
||||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||||
@@ -55,12 +54,16 @@ public class PreferenceConfiguration {
|
|||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||||
private static final boolean ONLY_L3_R3_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_DISABLE_FRAME_DROP = false;
|
||||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||||
private static final boolean DEFAULT_ENABLE_PIP = 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_BIND_ALL_USB = false;
|
||||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
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;
|
||||||
|
|
||||||
public static final int FORCE_H265_ON = -1;
|
public static final int FORCE_H265_ON = -1;
|
||||||
public static final int AUTOSELECT_H265 = 0;
|
public static final int AUTOSELECT_H265 = 0;
|
||||||
@@ -75,41 +78,103 @@ public class PreferenceConfiguration {
|
|||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
public boolean onscreenController;
|
public boolean onscreenController;
|
||||||
public boolean onlyL3R3;
|
public boolean onlyL3R3;
|
||||||
public boolean batterySaver;
|
|
||||||
public boolean disableFrameDrop;
|
public boolean disableFrameDrop;
|
||||||
public boolean enableHdr;
|
public boolean enableHdr;
|
||||||
public boolean enablePip;
|
public boolean enablePip;
|
||||||
|
public boolean enablePerfOverlay;
|
||||||
public boolean bindAllUsb;
|
public boolean bindAllUsb;
|
||||||
public boolean mouseEmulation;
|
public boolean mouseEmulation;
|
||||||
|
public boolean mouseNavButtons;
|
||||||
|
public boolean unlockFps;
|
||||||
|
public boolean vibrateOsc;
|
||||||
|
public boolean vibrateFallbackToDevice;
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
private static int getHeightFromResolutionString(String resString) {
|
||||||
if (resFpsString.equals("360p30")) {
|
if (resString.equalsIgnoreCase("360p")) {
|
||||||
return BITRATE_DEFAULT_360_30;
|
return 360;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("360p60")) {
|
else if (resString.equalsIgnoreCase("480p")) {
|
||||||
return BITRATE_DEFAULT_360_60;
|
return 480;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("720p30")) {
|
else if (resString.equalsIgnoreCase("720p")) {
|
||||||
return BITRATE_DEFAULT_720_30;
|
return 720;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("720p60")) {
|
else if (resString.equalsIgnoreCase("1080p")) {
|
||||||
return BITRATE_DEFAULT_720_60;
|
return 1080;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("1080p30")) {
|
else if (resString.equalsIgnoreCase("1440p")) {
|
||||||
return BITRATE_DEFAULT_1080_30;
|
return 1440;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("1080p60")) {
|
else if (resString.equalsIgnoreCase("4K")) {
|
||||||
return BITRATE_DEFAULT_1080_60;
|
return 2160;
|
||||||
}
|
|
||||||
else if (resFpsString.equals("4K30")) {
|
|
||||||
return BITRATE_DEFAULT_4K_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("4K60")) {
|
|
||||||
return BITRATE_DEFAULT_4K_60;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
// Should be unreachable
|
||||||
return DEFAULT_BITRATE;
|
return 720;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getWidthFromResolutionString(String resString) {
|
||||||
|
int height = getHeightFromResolutionString(resString);
|
||||||
|
if (height == 480) {
|
||||||
|
// This isn't an exact 16:9 resolution
|
||||||
|
return 854;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (height * 16) / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getResolutionString(int width, int height) {
|
||||||
|
switch (height) {
|
||||||
|
case 360:
|
||||||
|
return "360p";
|
||||||
|
case 480:
|
||||||
|
return "480p";
|
||||||
|
default:
|
||||||
|
case 720:
|
||||||
|
return "720p";
|
||||||
|
case 1080:
|
||||||
|
return "1080p";
|
||||||
|
case 1440:
|
||||||
|
return "1440p";
|
||||||
|
case 2160:
|
||||||
|
return "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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +200,9 @@ public class PreferenceConfiguration {
|
|||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
return getDefaultBitrate(
|
||||||
|
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
||||||
|
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getVideoFormatValue(Context context) {
|
private static int getVideoFormatValue(Context context) {
|
||||||
@@ -163,9 +230,12 @@ public class PreferenceConfiguration {
|
|||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove(BITRATE_PREF_STRING)
|
.remove(BITRATE_PREF_STRING)
|
||||||
.remove(BITRATE_PREF_OLD_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(VIDEO_FORMAT_PREF_STRING)
|
||||||
.remove(ENABLE_HDR_PREF_STRING)
|
.remove(ENABLE_HDR_PREF_STRING)
|
||||||
|
.remove(UNLOCK_FPS_STRING)
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,59 +243,76 @@ public class PreferenceConfiguration {
|
|||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
|
// Migrate legacy preferences to the new locations
|
||||||
|
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);
|
||||||
|
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||||
|
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||||
|
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||||
if (config.bitrate == 0) {
|
if (config.bitrate == 0) {
|
||||||
config.bitrate = getDefaultBitrate(context);
|
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;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
config.videoFormat = getVideoFormatValue(context);
|
||||||
|
|
||||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
@@ -244,12 +331,16 @@ public class PreferenceConfiguration {
|
|||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
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.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||||
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
|
|
||||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
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.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
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.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
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);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.media.MediaCodecInfo;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.os.Handler;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
@@ -14,6 +15,9 @@ import android.preference.PreferenceManager;
|
|||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.util.Range;
|
import android.util.Range;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
@@ -24,6 +28,12 @@ import com.limelight.utils.UiHelper;
|
|||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
private PreferenceConfiguration previousPrefs;
|
||||||
|
|
||||||
|
void reloadSettings() {
|
||||||
|
getFragmentManager().beginTransaction().replace(
|
||||||
|
R.id.stream_settings, new SettingsFragment()
|
||||||
|
).commit();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -33,9 +43,7 @@ public class StreamSettings extends Activity {
|
|||||||
UiHelper.setLocale(this);
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
setContentView(R.layout.activity_stream_settings);
|
||||||
getFragmentManager().beginTransaction().replace(
|
reloadSettings();
|
||||||
R.id.stream_settings, new SettingsFragment()
|
|
||||||
).commit();
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
}
|
}
|
||||||
@@ -58,12 +66,20 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
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;
|
int matchingCount = 0;
|
||||||
|
|
||||||
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
|
|
||||||
// Count the number of matching entries we'll be removing
|
// Count the number of matching entries we'll be removing
|
||||||
for (CharSequence seq : pref.getEntryValues()) {
|
for (CharSequence seq : pref.getEntryValues()) {
|
||||||
if (seq.toString().startsWith(prefix)) {
|
if (seq.toString().equalsIgnoreCase(value)) {
|
||||||
matchingCount++;
|
matchingCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,8 +89,8 @@ public class StreamSettings extends Activity {
|
|||||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||||
int outIndex = 0;
|
int outIndex = 0;
|
||||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||||
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
|
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||||
// Skip matching prefixes
|
// Skip matching values
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +99,38 @@ public class StreamSettings extends Activity {
|
|||||||
outIndex++;
|
outIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||||
|
onMatched.run();
|
||||||
|
}
|
||||||
|
|
||||||
// Update the preference with the new list
|
// Update the preference with the new list
|
||||||
pref.setEntries(entries);
|
pref.setEntries(entries);
|
||||||
pref.setEntryValues(entryValues);
|
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
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -110,6 +153,8 @@ public class StreamSettings extends Activity {
|
|||||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int maxSupportedFps = 0;
|
||||||
|
|
||||||
// Hide non-supported resolution/FPS combinations
|
// Hide non-supported resolution/FPS combinations
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||||
@@ -134,9 +179,16 @@ public class StreamSettings extends Activity {
|
|||||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||||
maxSupportedResW = 3840;
|
maxSupportedResW = 3840;
|
||||||
}
|
}
|
||||||
|
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
||||||
|
maxSupportedResW = 2560;
|
||||||
|
}
|
||||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
||||||
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
|
// This must be called to do runtime initialization before calling functions that evaluate
|
||||||
@@ -186,20 +238,102 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
||||||
|
|
||||||
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
|
|
||||||
if (maxSupportedResW != 0) {
|
if (maxSupportedResW != 0) {
|
||||||
if (maxSupportedResW < 3840) {
|
if (maxSupportedResW < 3840) {
|
||||||
// 4K is unsupported
|
// 4K is unsupported
|
||||||
removeResolution(resPref, "4K");
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
||||||
|
resetBitrateToDefault(prefs, null, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (maxSupportedResW < 2560) {
|
||||||
|
// 1440p is unsupported
|
||||||
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
||||||
|
resetBitrateToDefault(prefs, null, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (maxSupportedResW < 1920) {
|
if (maxSupportedResW < 1920) {
|
||||||
// 1080p is unsupported
|
// 1080p is unsupported
|
||||||
removeResolution(resPref, "1080p");
|
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
|
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
||||||
|
resetBitrateToDefault(prefs, null, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Never remove 720p
|
// 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 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_basic_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
|
// Remove HDR preference for devices below Nougat
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
LimeLog.info("Excluding HDR toggle based on OS");
|
LimeLog.info("Excluding HDR toggle based on OS");
|
||||||
@@ -213,9 +347,12 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
// We must now ensure our display is compatible with HDR10
|
||||||
boolean foundHdr10 = false;
|
boolean foundHdr10 = false;
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
if (hdrCaps != null) {
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||||
foundHdr10 = true;
|
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||||
|
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||||
|
foundHdr10 = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,18 +366,27 @@ public class StreamSettings extends Activity {
|
|||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
// Add a listener to the FPS and resolution preference
|
||||||
// so the bitrate can be auto-adjusted
|
// so the bitrate can be auto-adjusted
|
||||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||||
String valueStr = (String) newValue;
|
String valueStr = (String) newValue;
|
||||||
|
|
||||||
// Write the new bitrate value
|
// Write the new bitrate value
|
||||||
prefs.edit()
|
resetBitrateToDefault(prefs, valueStr, null);
|
||||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
|
||||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
// Allow the original preference change to take place
|
||||||
.apply();
|
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
|
// Allow the original preference change to take place
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ package com.limelight.ui;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
|
|
||||||
public class StreamView extends SurfaceView {
|
public class StreamView extends SurfaceView {
|
||||||
private double desiredAspectRatio;
|
private double desiredAspectRatio;
|
||||||
|
private InputCallbacks inputCallbacks;
|
||||||
|
|
||||||
public void setDesiredAspectRatio(double aspectRatio) {
|
public void setDesiredAspectRatio(double aspectRatio) {
|
||||||
this.desiredAspectRatio = aspectRatio;
|
this.desiredAspectRatio = aspectRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setInputCallbacks(InputCallbacks callbacks) {
|
||||||
|
this.inputCallbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
public StreamView(Context context) {
|
public StreamView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -52,4 +58,29 @@ public class StreamView extends SurfaceView {
|
|||||||
|
|
||||||
setMeasuredDimension(measuredWidth, measuredHeight);
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.
|
||||||
|
if (inputCallbacks != null) {
|
||||||
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||||
|
if (inputCallbacks.handleKeyDown(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||||
|
if (inputCallbacks.handleKeyUp(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onKeyPreIme(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface InputCallbacks {
|
||||||
|
boolean handleKeyUp(KeyEvent event);
|
||||||
|
boolean handleKeyDown(KeyEvent event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import android.app.Activity;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
import com.limelight.Game;
|
import com.limelight.Game;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.ShortcutTrampoline;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
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.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
|
||||||
public class ServerHelper {
|
public class ServerHelper {
|
||||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
return computer.activeAddress;
|
||||||
computer.localAddress : computer.remoteAddress;
|
}
|
||||||
|
|
||||||
|
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,
|
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_ID, app.getAppId());
|
||||||
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
|
||||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
|
|
||||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
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;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
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));
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final String address,
|
final ComputerDetails computer,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
final Runnable onComplete) {
|
final Runnable onComplete) {
|
||||||
@@ -54,8 +88,8 @@ public class ServerHelper {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
httpConn = new NvHTTP(address,
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||||
if (httpConn.quitApp()) {
|
if (httpConn.quitApp()) {
|
||||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||||
} else {
|
} else {
|
||||||
@@ -74,8 +108,9 @@ public class ServerHelper {
|
|||||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
message = parent.getResources().getString(R.string.error_404);
|
message = parent.getResources().getString(R.string.error_404);
|
||||||
} catch (Exception e) {
|
} catch (IOException | XmlPullParserException e) {
|
||||||
message = e.getMessage();
|
message = e.getMessage();
|
||||||
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
if (onComplete != null) {
|
if (onComplete != null) {
|
||||||
onComplete.run();
|
onComplete.run();
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ShortcutInfo;
|
import android.content.pm.ShortcutInfo;
|
||||||
import android.content.pm.ShortcutManager;
|
import android.content.pm.ShortcutManager;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Icon;
|
import android.graphics.drawable.Icon;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.AppViewShortcutTrampoline;
|
import com.limelight.ShortcutTrampoline;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -20,9 +22,10 @@ import java.util.List;
|
|||||||
public class ShortcutHelper {
|
public class ShortcutHelper {
|
||||||
|
|
||||||
private final ShortcutManager sm;
|
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;
|
this.context = context;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
sm = context.getSystemService(ShortcutManager.class);
|
sm = context.getSystemService(ShortcutManager.class);
|
||||||
@@ -30,6 +33,7 @@ public class ShortcutHelper {
|
|||||||
else {
|
else {
|
||||||
sm = null;
|
sm = null;
|
||||||
}
|
}
|
||||||
|
this.tvChannelHelper = new TvChannelHelper(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
@@ -78,40 +82,39 @@ public class ShortcutHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reportShortcutUsed(String id) {
|
public void reportComputerShortcutUsed(ComputerDetails computer) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
ShortcutInfo sinfo = getInfoForId(id);
|
if (getInfoForId(computer.uuid) != null) {
|
||||||
if (sinfo != null) {
|
sm.reportShortcutUsed(computer.uuid);
|
||||||
sm.reportShortcutUsed(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
public void reportGameLaunched(ComputerDetails computer, NvApp app) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
tvChannelHelper.createTvChannel(computer);
|
||||||
Intent i = new Intent(context, AppViewShortcutTrampoline.class);
|
tvChannelHelper.addGameToChannel(computer, app);
|
||||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
}
|
||||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
|
||||||
i.setAction(Intent.ACTION_DEFAULT);
|
|
||||||
|
|
||||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
public void createAppViewShortcut(ComputerDetails computer, boolean forceAdd, boolean newlyPaired) {
|
||||||
.setIntent(i)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
.setShortLabel(computerName)
|
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, computer.uuid)
|
||||||
.setLongLabel(computerName)
|
.setIntent(ServerHelper.createPcShortcutIntent(context, computer))
|
||||||
|
.setShortLabel(computer.name)
|
||||||
|
.setLongLabel(computer.name)
|
||||||
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
ShortcutInfo existingSinfo = getInfoForId(id);
|
ShortcutInfo existingSinfo = getInfoForId(computer.uuid);
|
||||||
if (existingSinfo != null) {
|
if (existingSinfo != null) {
|
||||||
// Update in place
|
// Update in place
|
||||||
sm.updateShortcuts(Collections.singletonList(sinfo));
|
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
|
// 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
|
// 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.
|
// 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,
|
// 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
|
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||||
// to interact with this PC.
|
// to interact with this PC.
|
||||||
@@ -121,16 +124,70 @@ 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) {
|
public void createAppViewShortcutForOnlineHost(ComputerDetails details) {
|
||||||
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
ShortcutInfo sinfo = getInfoForId(id);
|
// Delete the computer shortcut itself
|
||||||
if (sinfo != null) {
|
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);
|
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,18 +7,22 @@ import android.content.Context;
|
|||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Insets;
|
||||||
|
import android.os.Build;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
private static final int TV_VERTICAL_PADDING_DP = 15;
|
||||||
private static final int TV_VERTICAL_PADDING_DP = 27;
|
private static final int TV_HORIZONTAL_PADDING_DP = 15;
|
||||||
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
|
||||||
|
|
||||||
public static void setLocale(Activity activity)
|
public static void setLocale(Activity activity)
|
||||||
{
|
{
|
||||||
@@ -42,7 +46,24 @@ 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);
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
|
||||||
@@ -57,6 +78,45 @@ public class UiHelper {
|
|||||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
// Allow this non-streaming activity to layout under notches.
|
||||||
|
//
|
||||||
|
// We should NOT do this for the Game activity unless
|
||||||
|
// the user specifically opts in, because it can obscure
|
||||||
|
// parts of the streaming surface.
|
||||||
|
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
public static void showDecoderCrashDialog(Activity activity) {
|
||||||
@@ -123,4 +183,32 @@ public class UiHelper {
|
|||||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||||
.show();
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
#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", "(I)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", "(IJ)V");
|
||||||
|
BridgeClConnectionStartedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionStarted", "()V");
|
||||||
|
BridgeClConnectionTerminatedMethod = (*env)->GetStaticMethodID(env, clazz, "bridgeClConnectionTerminated", "(J)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;
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSetupMethod, videoFormat, width, height, redrawRate);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStartMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeDrStop(void) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrStopMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeDrCleanup(void) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
(*env)->DeleteGlobalRef(env, DecodedFrameBuffer);
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeDrCleanupMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BridgeDrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return DR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeDrSubmitDecodeUnitMethod,
|
||||||
|
DecodedFrameBuffer, offset, BUFFER_TYPE_PICDATA,
|
||||||
|
decodeUnit->frameNumber,
|
||||||
|
decodeUnit->receiveTimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BridgeArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = (*env)->CallStaticIntMethod(env, GlobalBridgeClass, BridgeArInitMethod, audioConfiguration);
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
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();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStartMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeArStop(void) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArStopMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeArCleanup() {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
opus_multistream_decoder_destroy(Decoder);
|
||||||
|
|
||||||
|
(*env)->DeleteGlobalRef(env, DecodedAudioBuffer);
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArCleanupMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeArDecodeAndPlaySample(char* sampleData, int sampleLength) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jshort* decodedData = (*env)->GetShortArrayElements(env, DecodedAudioBuffer, 0);
|
||||||
|
|
||||||
|
int decodeLen = opus_multistream_decode(Decoder,
|
||||||
|
(const unsigned char*)sampleData,
|
||||||
|
sampleLength,
|
||||||
|
decodedData,
|
||||||
|
OpusConfig.samplesPerFrame,
|
||||||
|
0);
|
||||||
|
if (decodeLen > 0) {
|
||||||
|
// We must release the array elements first to ensure the data is copied before the callback
|
||||||
|
(*env)->ReleaseShortArrayElements(env, DecodedAudioBuffer, decodedData, 0);
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeArPlaySampleMethod, DecodedAudioBuffer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We can abort here to avoid the copy back since no data was modified
|
||||||
|
(*env)->ReleaseShortArrayElements(env, DecodedAudioBuffer, decodedData, JNI_ABORT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClStageStarting(int stage) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageStartingMethod, stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClStageComplete(int stage) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageCompleteMethod, stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClStageFailed(int stage, long errorCode) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClStageFailedMethod, stage, errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClConnectionStarted(void) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStartedMethod, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClConnectionTerminated(long errorCode) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionTerminatedMethod, errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClRumbleMethod, controllerNumber, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BridgeClConnectionStatusUpdate(int connectionStatus) {
|
||||||
|
JNIEnv* env = GetThreadEnv();
|
||||||
|
|
||||||
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*env)->CallStaticVoidMethod(env, GlobalBridgeClass, BridgeClConnectionStatusUpdateMethod, connectionStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -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 */
|
||||||
@@ -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 */
|
||||||
@@ -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 */
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
/* Copyright (c) 2011 Xiph.Org Foundation
|
||||||
|
Written by Jean-Marc Valin */
|
||||||
|
/*
|
||||||
|
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_multistream.h
|
||||||
|
* @brief Opus reference implementation multistream API
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OPUS_MULTISTREAM_H
|
||||||
|
#define OPUS_MULTISTREAM_H
|
||||||
|
|
||||||
|
#include "opus.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** @cond OPUS_INTERNAL_DOC */
|
||||||
|
|
||||||
|
/** Macros to trigger compilation errors when the wrong types are provided to a
|
||||||
|
* CTL. */
|
||||||
|
/**@{*/
|
||||||
|
#define __opus_check_encstate_ptr(ptr) ((ptr) + ((ptr) - (OpusEncoder**)(ptr)))
|
||||||
|
#define __opus_check_decstate_ptr(ptr) ((ptr) + ((ptr) - (OpusDecoder**)(ptr)))
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/** These are the actual encoder and decoder CTL ID numbers.
|
||||||
|
* They should not be used directly by applications.
|
||||||
|
* In general, SETs should be even and GETs should be odd.*/
|
||||||
|
/**@{*/
|
||||||
|
#define OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST 5120
|
||||||
|
#define OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST 5122
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/** @endcond */
|
||||||
|
|
||||||
|
/** @defgroup opus_multistream_ctls Multistream specific encoder and decoder CTLs
|
||||||
|
*
|
||||||
|
* These are convenience macros that are specific to the
|
||||||
|
* opus_multistream_encoder_ctl() and opus_multistream_decoder_ctl()
|
||||||
|
* interface.
|
||||||
|
* The CTLs from @ref opus_genericctls, @ref opus_encoderctls, and
|
||||||
|
* @ref opus_decoderctls may be applied to a multistream encoder or decoder as
|
||||||
|
* well.
|
||||||
|
* In addition, you may retrieve the encoder or decoder state for an specific
|
||||||
|
* stream via #OPUS_MULTISTREAM_GET_ENCODER_STATE or
|
||||||
|
* #OPUS_MULTISTREAM_GET_DECODER_STATE and apply CTLs to it individually.
|
||||||
|
*/
|
||||||
|
/**@{*/
|
||||||
|
|
||||||
|
/** Gets the encoder state for an individual stream of a multistream encoder.
|
||||||
|
* @param[in] x <tt>opus_int32</tt>: The index of the stream whose encoder you
|
||||||
|
* wish to retrieve.
|
||||||
|
* This must be non-negative and less than
|
||||||
|
* the <code>streams</code> parameter used
|
||||||
|
* to initialize the encoder.
|
||||||
|
* @param[out] y <tt>OpusEncoder**</tt>: Returns a pointer to the given
|
||||||
|
* encoder state.
|
||||||
|
* @retval OPUS_BAD_ARG The index of the requested stream was out of range.
|
||||||
|
* @hideinitializer
|
||||||
|
*/
|
||||||
|
#define OPUS_MULTISTREAM_GET_ENCODER_STATE(x,y) OPUS_MULTISTREAM_GET_ENCODER_STATE_REQUEST, __opus_check_int(x), __opus_check_encstate_ptr(y)
|
||||||
|
|
||||||
|
/** Gets the decoder state for an individual stream of a multistream decoder.
|
||||||
|
* @param[in] x <tt>opus_int32</tt>: The index of the stream whose decoder you
|
||||||
|
* wish to retrieve.
|
||||||
|
* This must be non-negative and less than
|
||||||
|
* the <code>streams</code> parameter used
|
||||||
|
* to initialize the decoder.
|
||||||
|
* @param[out] y <tt>OpusDecoder**</tt>: Returns a pointer to the given
|
||||||
|
* decoder state.
|
||||||
|
* @retval OPUS_BAD_ARG The index of the requested stream was out of range.
|
||||||
|
* @hideinitializer
|
||||||
|
*/
|
||||||
|
#define OPUS_MULTISTREAM_GET_DECODER_STATE(x,y) OPUS_MULTISTREAM_GET_DECODER_STATE_REQUEST, __opus_check_int(x), __opus_check_decstate_ptr(y)
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/** @defgroup opus_multistream Opus Multistream API
|
||||||
|
* @{
|
||||||
|
*
|
||||||
|
* The multistream API allows individual Opus streams to be combined into a
|
||||||
|
* single packet, enabling support for up to 255 channels. Unlike an
|
||||||
|
* elementary Opus stream, the encoder and decoder must negotiate the channel
|
||||||
|
* configuration before the decoder can successfully interpret the data in the
|
||||||
|
* packets produced by the encoder. Some basic information, such as packet
|
||||||
|
* duration, can be computed without any special negotiation.
|
||||||
|
*
|
||||||
|
* The format for multistream Opus packets is defined in
|
||||||
|
* <a href="https://tools.ietf.org/html/rfc7845">RFC 7845</a>
|
||||||
|
* and is based on the self-delimited Opus framing described in Appendix B of
|
||||||
|
* <a href="https://tools.ietf.org/html/rfc6716">RFC 6716</a>.
|
||||||
|
* Normal Opus packets are just a degenerate case of multistream Opus packets,
|
||||||
|
* and can be encoded or decoded with the multistream API by setting
|
||||||
|
* <code>streams</code> to <code>1</code> when initializing the encoder or
|
||||||
|
* decoder.
|
||||||
|
*
|
||||||
|
* Multistream Opus streams can contain up to 255 elementary Opus streams.
|
||||||
|
* These may be either "uncoupled" or "coupled", indicating that the decoder
|
||||||
|
* is configured to decode them to either 1 or 2 channels, respectively.
|
||||||
|
* The streams are ordered so that all coupled streams appear at the
|
||||||
|
* beginning.
|
||||||
|
*
|
||||||
|
* A <code>mapping</code> table defines which decoded channel <code>i</code>
|
||||||
|
* should be used for each input/output (I/O) channel <code>j</code>. This table is
|
||||||
|
* typically provided as an unsigned char array.
|
||||||
|
* Let <code>i = mapping[j]</code> be the index for I/O channel <code>j</code>.
|
||||||
|
* If <code>i < 2*coupled_streams</code>, then I/O channel <code>j</code> is
|
||||||
|
* encoded as the left channel of stream <code>(i/2)</code> if <code>i</code>
|
||||||
|
* is even, or as the right channel of stream <code>(i/2)</code> if
|
||||||
|
* <code>i</code> is odd. Otherwise, I/O channel <code>j</code> is encoded as
|
||||||
|
* mono in stream <code>(i - coupled_streams)</code>, unless it has the special
|
||||||
|
* value 255, in which case it is omitted from the encoding entirely (the
|
||||||
|
* decoder will reproduce it as silence). Each value <code>i</code> must either
|
||||||
|
* be the special value 255 or be less than <code>streams + coupled_streams</code>.
|
||||||
|
*
|
||||||
|
* The output channels specified by the encoder
|
||||||
|
* should use the
|
||||||
|
* <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810004.3.9">Vorbis
|
||||||
|
* channel ordering</a>. A decoder may wish to apply an additional permutation
|
||||||
|
* to the mapping the encoder used to achieve a different output channel
|
||||||
|
* order (e.g. for outputing in WAV order).
|
||||||
|
*
|
||||||
|
* Each multistream packet contains an Opus packet for each stream, and all of
|
||||||
|
* the Opus packets in a single multistream packet must have the same
|
||||||
|
* duration. Therefore the duration of a multistream packet can be extracted
|
||||||
|
* from the TOC sequence of the first stream, which is located at the
|
||||||
|
* beginning of the packet, just like an elementary Opus stream:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* int nb_samples;
|
||||||
|
* int nb_frames;
|
||||||
|
* nb_frames = opus_packet_get_nb_frames(data, len);
|
||||||
|
* if (nb_frames < 1)
|
||||||
|
* return nb_frames;
|
||||||
|
* nb_samples = opus_packet_get_samples_per_frame(data, 48000) * nb_frames;
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* The general encoding and decoding process proceeds exactly the same as in
|
||||||
|
* the normal @ref opus_encoder and @ref opus_decoder APIs.
|
||||||
|
* See their documentation for an overview of how to use the corresponding
|
||||||
|
* multistream functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Opus multistream encoder state.
|
||||||
|
* This contains the complete state of a multistream Opus encoder.
|
||||||
|
* It is position independent and can be freely copied.
|
||||||
|
* @see opus_multistream_encoder_create
|
||||||
|
* @see opus_multistream_encoder_init
|
||||||
|
*/
|
||||||
|
typedef struct OpusMSEncoder OpusMSEncoder;
|
||||||
|
|
||||||
|
/** Opus multistream decoder state.
|
||||||
|
* This contains the complete state of a multistream Opus decoder.
|
||||||
|
* It is position independent and can be freely copied.
|
||||||
|
* @see opus_multistream_decoder_create
|
||||||
|
* @see opus_multistream_decoder_init
|
||||||
|
*/
|
||||||
|
typedef struct OpusMSDecoder OpusMSDecoder;
|
||||||
|
|
||||||
|
/**\name Multistream encoder functions */
|
||||||
|
/**@{*/
|
||||||
|
|
||||||
|
/** Gets the size of an OpusMSEncoder structure.
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams to encode from the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of coupled (2 channel) streams
|
||||||
|
* to encode.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* encoded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @returns The size in bytes on success, or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on error.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_encoder_get_size(
|
||||||
|
int streams,
|
||||||
|
int coupled_streams
|
||||||
|
);
|
||||||
|
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_surround_encoder_get_size(
|
||||||
|
int channels,
|
||||||
|
int mapping_family
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/** Allocates and initializes a multistream encoder state.
|
||||||
|
* Call opus_multistream_encoder_destroy() to release
|
||||||
|
* this object when finished.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate of the input signal (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels in the input signal.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be greater than the number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams to encode from the
|
||||||
|
* input.
|
||||||
|
* This must be no more than the number of channels.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of coupled (2 channel) streams
|
||||||
|
* to encode.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* encoded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than the number of input channels.
|
||||||
|
* @param[in] mapping <code>const unsigned char[channels]</code>: Mapping from
|
||||||
|
* encoded channels to input channels, as described in
|
||||||
|
* @ref opus_multistream. As an extra constraint, the
|
||||||
|
* multistream encoder does not allow encoding coupled
|
||||||
|
* streams for which one channel is unused since this
|
||||||
|
* is never a good idea.
|
||||||
|
* @param application <tt>int</tt>: The target encoder application.
|
||||||
|
* This must be one of the following:
|
||||||
|
* <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>
|
||||||
|
* @param[out] error <tt>int *</tt>: Returns #OPUS_OK on success, or an error
|
||||||
|
* code (see @ref opus_errorcodes) on
|
||||||
|
* failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_encoder_create(
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams,
|
||||||
|
const unsigned char *mapping,
|
||||||
|
int application,
|
||||||
|
int *error
|
||||||
|
) OPUS_ARG_NONNULL(5);
|
||||||
|
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_surround_encoder_create(
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int mapping_family,
|
||||||
|
int *streams,
|
||||||
|
int *coupled_streams,
|
||||||
|
unsigned char *mapping,
|
||||||
|
int application,
|
||||||
|
int *error
|
||||||
|
) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6);
|
||||||
|
|
||||||
|
/** Initialize a previously allocated multistream encoder state.
|
||||||
|
* The memory pointed to by \a st must be at least the size returned by
|
||||||
|
* opus_multistream_encoder_get_size().
|
||||||
|
* This is intended for applications which use their own allocator instead of
|
||||||
|
* malloc.
|
||||||
|
* To reset a previously initialized state, use the #OPUS_RESET_STATE CTL.
|
||||||
|
* @see opus_multistream_encoder_create
|
||||||
|
* @see opus_multistream_encoder_get_size
|
||||||
|
* @param st <tt>OpusMSEncoder*</tt>: Multistream encoder state to initialize.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate of the input signal (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels in the input signal.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be greater than the number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams to encode from the
|
||||||
|
* input.
|
||||||
|
* This must be no more than the number of channels.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of coupled (2 channel) streams
|
||||||
|
* to encode.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* encoded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than the number of input channels.
|
||||||
|
* @param[in] mapping <code>const unsigned char[channels]</code>: Mapping from
|
||||||
|
* encoded channels to input channels, as described in
|
||||||
|
* @ref opus_multistream. As an extra constraint, the
|
||||||
|
* multistream encoder does not allow encoding coupled
|
||||||
|
* streams for which one channel is unused since this
|
||||||
|
* is never a good idea.
|
||||||
|
* @param application <tt>int</tt>: The target encoder application.
|
||||||
|
* This must be one of the following:
|
||||||
|
* <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>
|
||||||
|
* @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes)
|
||||||
|
* on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_multistream_encoder_init(
|
||||||
|
OpusMSEncoder *st,
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams,
|
||||||
|
const unsigned char *mapping,
|
||||||
|
int application
|
||||||
|
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6);
|
||||||
|
|
||||||
|
OPUS_EXPORT int opus_multistream_surround_encoder_init(
|
||||||
|
OpusMSEncoder *st,
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int mapping_family,
|
||||||
|
int *streams,
|
||||||
|
int *coupled_streams,
|
||||||
|
unsigned char *mapping,
|
||||||
|
int application
|
||||||
|
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6) OPUS_ARG_NONNULL(7);
|
||||||
|
|
||||||
|
/** Encodes a multistream Opus frame.
|
||||||
|
* @param st <tt>OpusMSEncoder*</tt>: Multistream encoder state.
|
||||||
|
* @param[in] pcm <tt>const opus_int16*</tt>: The input signal as interleaved
|
||||||
|
* samples.
|
||||||
|
* This must contain
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param 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 int opus_multistream_encode(
|
||||||
|
OpusMSEncoder *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 a multistream Opus frame from floating point input.
|
||||||
|
* @param st <tt>OpusMSEncoder*</tt>: Multistream encoder state.
|
||||||
|
* @param[in] pcm <tt>const float*</tt>: The input signal as interleaved
|
||||||
|
* samples 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.
|
||||||
|
* This must contain
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param 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 int opus_multistream_encode_float(
|
||||||
|
OpusMSEncoder *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>OpusMSEncoder</code> allocated by
|
||||||
|
* opus_multistream_encoder_create().
|
||||||
|
* @param st <tt>OpusMSEncoder*</tt>: Multistream encoder state to be freed.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT void opus_multistream_encoder_destroy(OpusMSEncoder *st);
|
||||||
|
|
||||||
|
/** Perform a CTL function on a multistream Opus encoder.
|
||||||
|
*
|
||||||
|
* Generally the request and subsequent arguments are generated by a
|
||||||
|
* convenience macro.
|
||||||
|
* @param st <tt>OpusMSEncoder*</tt>: Multistream encoder state.
|
||||||
|
* @param request This and all remaining parameters should be replaced by one
|
||||||
|
* of the convenience macros in @ref opus_genericctls,
|
||||||
|
* @ref opus_encoderctls, or @ref opus_multistream_ctls.
|
||||||
|
* @see opus_genericctls
|
||||||
|
* @see opus_encoderctls
|
||||||
|
* @see opus_multistream_ctls
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_multistream_encoder_ctl(OpusMSEncoder *st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/**\name Multistream decoder functions */
|
||||||
|
/**@{*/
|
||||||
|
|
||||||
|
/** Gets the size of an <code>OpusMSDecoder</code> structure.
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams coded in the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number streams to decode as coupled
|
||||||
|
* (2 channel) streams.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @returns The size in bytes on success, or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on error.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_decoder_get_size(
|
||||||
|
int streams,
|
||||||
|
int coupled_streams
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Allocates and initializes a multistream decoder state.
|
||||||
|
* Call opus_multistream_decoder_destroy() to release
|
||||||
|
* this object when finished.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate to decode at (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels to output.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be different from the number of coded
|
||||||
|
* channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams coded in the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of streams to decode as coupled
|
||||||
|
* (2 channel) streams.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @param[in] mapping <code>const unsigned char[channels]</code>: Mapping from
|
||||||
|
* coded channels to output channels, as described in
|
||||||
|
* @ref opus_multistream.
|
||||||
|
* @param[out] error <tt>int *</tt>: Returns #OPUS_OK on success, or an error
|
||||||
|
* code (see @ref opus_errorcodes) on
|
||||||
|
* failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSDecoder *opus_multistream_decoder_create(
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams,
|
||||||
|
const unsigned char *mapping,
|
||||||
|
int *error
|
||||||
|
) OPUS_ARG_NONNULL(5);
|
||||||
|
|
||||||
|
/** Intialize a previously allocated decoder state object.
|
||||||
|
* The memory pointed to by \a st must be at least the size returned by
|
||||||
|
* opus_multistream_encoder_get_size().
|
||||||
|
* This is intended for applications which use their own allocator instead of
|
||||||
|
* malloc.
|
||||||
|
* To reset a previously initialized state, use the #OPUS_RESET_STATE CTL.
|
||||||
|
* @see opus_multistream_decoder_create
|
||||||
|
* @see opus_multistream_deocder_get_size
|
||||||
|
* @param st <tt>OpusMSEncoder*</tt>: Multistream encoder state to initialize.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate to decode at (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels to output.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be different from the number of coded
|
||||||
|
* channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams coded in the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of streams to decode as coupled
|
||||||
|
* (2 channel) streams.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @param[in] mapping <code>const unsigned char[channels]</code>: Mapping from
|
||||||
|
* coded channels to output channels, as described in
|
||||||
|
* @ref opus_multistream.
|
||||||
|
* @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes)
|
||||||
|
* on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_multistream_decoder_init(
|
||||||
|
OpusMSDecoder *st,
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams,
|
||||||
|
const unsigned char *mapping
|
||||||
|
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6);
|
||||||
|
|
||||||
|
/** Decode a multistream Opus packet.
|
||||||
|
* @param st <tt>OpusMSDecoder*</tt>: Multistream decoder state.
|
||||||
|
* @param[in] data <tt>const unsigned char*</tt>: Input payload.
|
||||||
|
* Use a <code>NULL</code>
|
||||||
|
* pointer to indicate packet
|
||||||
|
* loss.
|
||||||
|
* @param len <tt>opus_int32</tt>: Number of bytes in payload.
|
||||||
|
* @param[out] pcm <tt>opus_int16*</tt>: Output signal, with interleaved
|
||||||
|
* samples.
|
||||||
|
* This must contain room for
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param frame_size <tt>int</tt>: The number of samples per channel of
|
||||||
|
* available space in \a pcm.
|
||||||
|
* If this is less than the maximum packet duration
|
||||||
|
* (120 ms; 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 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 samples decoded on success or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode(
|
||||||
|
OpusMSDecoder *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 a multistream Opus packet with floating point output.
|
||||||
|
* @param st <tt>OpusMSDecoder*</tt>: Multistream decoder state.
|
||||||
|
* @param[in] data <tt>const unsigned char*</tt>: Input payload.
|
||||||
|
* Use a <code>NULL</code>
|
||||||
|
* pointer to indicate packet
|
||||||
|
* loss.
|
||||||
|
* @param len <tt>opus_int32</tt>: Number of bytes in payload.
|
||||||
|
* @param[out] pcm <tt>opus_int16*</tt>: Output signal, with interleaved
|
||||||
|
* samples.
|
||||||
|
* This must contain room for
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param frame_size <tt>int</tt>: The number of samples per channel of
|
||||||
|
* available space in \a pcm.
|
||||||
|
* If this is less than the maximum packet duration
|
||||||
|
* (120 ms; 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 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 samples decoded on success or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode_float(
|
||||||
|
OpusMSDecoder *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 a multistream Opus decoder.
|
||||||
|
*
|
||||||
|
* Generally the request and subsequent arguments are generated by a
|
||||||
|
* convenience macro.
|
||||||
|
* @param st <tt>OpusMSDecoder*</tt>: Multistream decoder state.
|
||||||
|
* @param request This and all remaining parameters should be replaced by one
|
||||||
|
* of the convenience macros in @ref opus_genericctls,
|
||||||
|
* @ref opus_decoderctls, or @ref opus_multistream_ctls.
|
||||||
|
* @see opus_genericctls
|
||||||
|
* @see opus_decoderctls
|
||||||
|
* @see opus_multistream_ctls
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_multistream_decoder_ctl(OpusMSDecoder *st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||||
|
|
||||||
|
/** Frees an <code>OpusMSDecoder</code> allocated by
|
||||||
|
* opus_multistream_decoder_create().
|
||||||
|
* @param st <tt>OpusMSDecoder</tt>: Multistream decoder state to be freed.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT void opus_multistream_decoder_destroy(OpusMSDecoder *st);
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OPUS_MULTISTREAM_H */
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
/* Copyright (c) 2017 Google Inc.
|
||||||
|
Written by Andrew Allen */
|
||||||
|
/*
|
||||||
|
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_projection.h
|
||||||
|
* @brief Opus projection reference API
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OPUS_PROJECTION_H
|
||||||
|
#define OPUS_PROJECTION_H
|
||||||
|
|
||||||
|
#include "opus_multistream.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** @cond OPUS_INTERNAL_DOC */
|
||||||
|
|
||||||
|
/** These are the actual encoder and decoder CTL ID numbers.
|
||||||
|
* They should not be used directly by applications.c
|
||||||
|
* In general, SETs should be even and GETs should be odd.*/
|
||||||
|
/**@{*/
|
||||||
|
#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST 6001
|
||||||
|
#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST 6003
|
||||||
|
#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST 6005
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
|
||||||
|
/** @endcond */
|
||||||
|
|
||||||
|
/** @defgroup opus_projection_ctls Projection specific encoder and decoder CTLs
|
||||||
|
*
|
||||||
|
* These are convenience macros that are specific to the
|
||||||
|
* opus_projection_encoder_ctl() and opus_projection_decoder_ctl()
|
||||||
|
* interface.
|
||||||
|
* The CTLs from @ref opus_genericctls, @ref opus_encoderctls,
|
||||||
|
* @ref opus_decoderctls, and @ref opus_multistream_ctls may be applied to a
|
||||||
|
* projection encoder or decoder as well.
|
||||||
|
*/
|
||||||
|
/**@{*/
|
||||||
|
|
||||||
|
/** Gets the gain (in dB. S7.8-format) of the demixing matrix from the encoder.
|
||||||
|
* @param[out] x <tt>opus_int32 *</tt>: Returns the gain (in dB. S7.8-format)
|
||||||
|
* of the demixing matrix.
|
||||||
|
* @hideinitializer
|
||||||
|
*/
|
||||||
|
#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_GAIN_REQUEST, __opus_check_int_ptr(x)
|
||||||
|
|
||||||
|
|
||||||
|
/** Gets the size in bytes of the demixing matrix from the encoder.
|
||||||
|
* @param[out] x <tt>opus_int32 *</tt>: Returns the size in bytes of the
|
||||||
|
* demixing matrix.
|
||||||
|
* @hideinitializer
|
||||||
|
*/
|
||||||
|
#define OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE(x) OPUS_PROJECTION_GET_DEMIXING_MATRIX_SIZE_REQUEST, __opus_check_int_ptr(x)
|
||||||
|
|
||||||
|
|
||||||
|
/** Copies the demixing matrix to the supplied pointer location.
|
||||||
|
* @param[out] x <tt>unsigned char *</tt>: Returns the demixing matrix to the
|
||||||
|
* supplied pointer location.
|
||||||
|
* @param y <tt>opus_int32</tt>: The size in bytes of the reserved memory at the
|
||||||
|
* pointer location.
|
||||||
|
* @hideinitializer
|
||||||
|
*/
|
||||||
|
#define OPUS_PROJECTION_GET_DEMIXING_MATRIX(x,y) OPUS_PROJECTION_GET_DEMIXING_MATRIX_REQUEST, x, __opus_check_int(y)
|
||||||
|
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/** Opus projection encoder state.
|
||||||
|
* This contains the complete state of a projection Opus encoder.
|
||||||
|
* It is position independent and can be freely copied.
|
||||||
|
* @see opus_projection_ambisonics_encoder_create
|
||||||
|
*/
|
||||||
|
typedef struct OpusProjectionEncoder OpusProjectionEncoder;
|
||||||
|
|
||||||
|
|
||||||
|
/** Opus projection decoder state.
|
||||||
|
* This contains the complete state of a projection Opus decoder.
|
||||||
|
* It is position independent and can be freely copied.
|
||||||
|
* @see opus_projection_decoder_create
|
||||||
|
* @see opus_projection_decoder_init
|
||||||
|
*/
|
||||||
|
typedef struct OpusProjectionDecoder OpusProjectionDecoder;
|
||||||
|
|
||||||
|
|
||||||
|
/**\name Projection encoder functions */
|
||||||
|
/**@{*/
|
||||||
|
|
||||||
|
/** Gets the size of an OpusProjectionEncoder structure.
|
||||||
|
* @param channels <tt>int</tt>: The total number of input channels to encode.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param mapping_family <tt>int</tt>: The mapping family to use for selecting
|
||||||
|
* the appropriate projection.
|
||||||
|
* @returns The size in bytes on success, or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on error.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_ambisonics_encoder_get_size(
|
||||||
|
int channels,
|
||||||
|
int mapping_family
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/** Allocates and initializes a projection encoder state.
|
||||||
|
* Call opus_projection_encoder_destroy() to release
|
||||||
|
* this object when finished.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate of the input signal (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels in the input signal.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be greater than the number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param mapping_family <tt>int</tt>: The mapping family to use for selecting
|
||||||
|
* the appropriate projection.
|
||||||
|
* @param[out] streams <tt>int *</tt>: The total number of streams that will
|
||||||
|
* be encoded from the input.
|
||||||
|
* @param[out] coupled_streams <tt>int *</tt>: Number of coupled (2 channel)
|
||||||
|
* streams that will be encoded from the input.
|
||||||
|
* @param application <tt>int</tt>: The target encoder application.
|
||||||
|
* This must be one of the following:
|
||||||
|
* <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>
|
||||||
|
* @param[out] error <tt>int *</tt>: Returns #OPUS_OK on success, or an error
|
||||||
|
* code (see @ref opus_errorcodes) on
|
||||||
|
* failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionEncoder *opus_projection_ambisonics_encoder_create(
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int mapping_family,
|
||||||
|
int *streams,
|
||||||
|
int *coupled_streams,
|
||||||
|
int application,
|
||||||
|
int *error
|
||||||
|
) OPUS_ARG_NONNULL(4) OPUS_ARG_NONNULL(5);
|
||||||
|
|
||||||
|
|
||||||
|
/** Initialize a previously allocated projection encoder state.
|
||||||
|
* The memory pointed to by \a st must be at least the size returned by
|
||||||
|
* opus_projection_ambisonics_encoder_get_size().
|
||||||
|
* This is intended for applications which use their own allocator instead of
|
||||||
|
* malloc.
|
||||||
|
* To reset a previously initialized state, use the #OPUS_RESET_STATE CTL.
|
||||||
|
* @see opus_projection_ambisonics_encoder_create
|
||||||
|
* @see opus_projection_ambisonics_encoder_get_size
|
||||||
|
* @param st <tt>OpusProjectionEncoder*</tt>: Projection encoder state to initialize.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate of the input signal (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels in the input signal.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be greater than the number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams to encode from the
|
||||||
|
* input.
|
||||||
|
* This must be no more than the number of channels.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of coupled (2 channel) streams
|
||||||
|
* to encode.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* encoded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than the number of input channels.
|
||||||
|
* @param application <tt>int</tt>: The target encoder application.
|
||||||
|
* This must be one of the following:
|
||||||
|
* <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>
|
||||||
|
* @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes)
|
||||||
|
* on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_projection_ambisonics_encoder_init(
|
||||||
|
OpusProjectionEncoder *st,
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int mapping_family,
|
||||||
|
int *streams,
|
||||||
|
int *coupled_streams,
|
||||||
|
int application
|
||||||
|
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(5) OPUS_ARG_NONNULL(6);
|
||||||
|
|
||||||
|
|
||||||
|
/** Encodes a projection Opus frame.
|
||||||
|
* @param st <tt>OpusProjectionEncoder*</tt>: Projection encoder state.
|
||||||
|
* @param[in] pcm <tt>const opus_int16*</tt>: The input signal as interleaved
|
||||||
|
* samples.
|
||||||
|
* This must contain
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param 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 int opus_projection_encode(
|
||||||
|
OpusProjectionEncoder *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 a projection Opus frame from floating point input.
|
||||||
|
* @param st <tt>OpusProjectionEncoder*</tt>: Projection encoder state.
|
||||||
|
* @param[in] pcm <tt>const float*</tt>: The input signal as interleaved
|
||||||
|
* samples 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.
|
||||||
|
* This must contain
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param 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 int opus_projection_encode_float(
|
||||||
|
OpusProjectionEncoder *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>OpusProjectionEncoder</code> allocated by
|
||||||
|
* opus_projection_ambisonics_encoder_create().
|
||||||
|
* @param st <tt>OpusProjectionEncoder*</tt>: Projection encoder state to be freed.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT void opus_projection_encoder_destroy(OpusProjectionEncoder *st);
|
||||||
|
|
||||||
|
|
||||||
|
/** Perform a CTL function on a projection Opus encoder.
|
||||||
|
*
|
||||||
|
* Generally the request and subsequent arguments are generated by a
|
||||||
|
* convenience macro.
|
||||||
|
* @param st <tt>OpusProjectionEncoder*</tt>: Projection encoder state.
|
||||||
|
* @param request This and all remaining parameters should be replaced by one
|
||||||
|
* of the convenience macros in @ref opus_genericctls,
|
||||||
|
* @ref opus_encoderctls, @ref opus_multistream_ctls, or
|
||||||
|
* @ref opus_projection_ctls
|
||||||
|
* @see opus_genericctls
|
||||||
|
* @see opus_encoderctls
|
||||||
|
* @see opus_multistream_ctls
|
||||||
|
* @see opus_projection_ctls
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_projection_encoder_ctl(OpusProjectionEncoder *st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||||
|
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/**\name Projection decoder functions */
|
||||||
|
/**@{*/
|
||||||
|
|
||||||
|
/** Gets the size of an <code>OpusProjectionDecoder</code> structure.
|
||||||
|
* @param channels <tt>int</tt>: The total number of output channels.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams coded in the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number streams to decode as coupled
|
||||||
|
* (2 channel) streams.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @returns The size in bytes on success, or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on error.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_projection_decoder_get_size(
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/** Allocates and initializes a projection decoder state.
|
||||||
|
* Call opus_projection_decoder_destroy() to release
|
||||||
|
* this object when finished.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate to decode at (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels to output.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be different from the number of coded
|
||||||
|
* channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams coded in the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of streams to decode as coupled
|
||||||
|
* (2 channel) streams.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @param[in] demixing_matrix <tt>const unsigned char[demixing_matrix_size]</tt>: Demixing matrix
|
||||||
|
* that mapping from coded channels to output channels,
|
||||||
|
* as described in @ref opus_projection and
|
||||||
|
* @ref opus_projection_ctls.
|
||||||
|
* @param demixing_matrix_size <tt>opus_int32</tt>: The size in bytes of the
|
||||||
|
* demixing matrix, as
|
||||||
|
* described in @ref
|
||||||
|
* opus_projection_ctls.
|
||||||
|
* @param[out] error <tt>int *</tt>: Returns #OPUS_OK on success, or an error
|
||||||
|
* code (see @ref opus_errorcodes) on
|
||||||
|
* failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusProjectionDecoder *opus_projection_decoder_create(
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams,
|
||||||
|
unsigned char *demixing_matrix,
|
||||||
|
opus_int32 demixing_matrix_size,
|
||||||
|
int *error
|
||||||
|
) OPUS_ARG_NONNULL(5);
|
||||||
|
|
||||||
|
|
||||||
|
/** Intialize a previously allocated projection decoder state object.
|
||||||
|
* The memory pointed to by \a st must be at least the size returned by
|
||||||
|
* opus_projection_decoder_get_size().
|
||||||
|
* This is intended for applications which use their own allocator instead of
|
||||||
|
* malloc.
|
||||||
|
* To reset a previously initialized state, use the #OPUS_RESET_STATE CTL.
|
||||||
|
* @see opus_projection_decoder_create
|
||||||
|
* @see opus_projection_deocder_get_size
|
||||||
|
* @param st <tt>OpusProjectionDecoder*</tt>: Projection encoder state to initialize.
|
||||||
|
* @param Fs <tt>opus_int32</tt>: Sampling rate to decode at (in Hz).
|
||||||
|
* This must be one of 8000, 12000, 16000,
|
||||||
|
* 24000, or 48000.
|
||||||
|
* @param channels <tt>int</tt>: Number of channels to output.
|
||||||
|
* This must be at most 255.
|
||||||
|
* It may be different from the number of coded
|
||||||
|
* channels (<code>streams +
|
||||||
|
* coupled_streams</code>).
|
||||||
|
* @param streams <tt>int</tt>: The total number of streams coded in the
|
||||||
|
* input.
|
||||||
|
* This must be no more than 255.
|
||||||
|
* @param coupled_streams <tt>int</tt>: Number of streams to decode as coupled
|
||||||
|
* (2 channel) streams.
|
||||||
|
* This must be no larger than the total
|
||||||
|
* number of streams.
|
||||||
|
* Additionally, The total number of
|
||||||
|
* coded channels (<code>streams +
|
||||||
|
* coupled_streams</code>) must be no
|
||||||
|
* more than 255.
|
||||||
|
* @param[in] demixing_matrix <tt>const unsigned char[demixing_matrix_size]</tt>: Demixing matrix
|
||||||
|
* that mapping from coded channels to output channels,
|
||||||
|
* as described in @ref opus_projection and
|
||||||
|
* @ref opus_projection_ctls.
|
||||||
|
* @param demixing_matrix_size <tt>opus_int32</tt>: The size in bytes of the
|
||||||
|
* demixing matrix, as
|
||||||
|
* described in @ref
|
||||||
|
* opus_projection_ctls.
|
||||||
|
* @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes)
|
||||||
|
* on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_projection_decoder_init(
|
||||||
|
OpusProjectionDecoder *st,
|
||||||
|
opus_int32 Fs,
|
||||||
|
int channels,
|
||||||
|
int streams,
|
||||||
|
int coupled_streams,
|
||||||
|
unsigned char *demixing_matrix,
|
||||||
|
opus_int32 demixing_matrix_size
|
||||||
|
) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6);
|
||||||
|
|
||||||
|
|
||||||
|
/** Decode a projection Opus packet.
|
||||||
|
* @param st <tt>OpusProjectionDecoder*</tt>: Projection decoder state.
|
||||||
|
* @param[in] data <tt>const unsigned char*</tt>: Input payload.
|
||||||
|
* Use a <code>NULL</code>
|
||||||
|
* pointer to indicate packet
|
||||||
|
* loss.
|
||||||
|
* @param len <tt>opus_int32</tt>: Number of bytes in payload.
|
||||||
|
* @param[out] pcm <tt>opus_int16*</tt>: Output signal, with interleaved
|
||||||
|
* samples.
|
||||||
|
* This must contain room for
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param frame_size <tt>int</tt>: The number of samples per channel of
|
||||||
|
* available space in \a pcm.
|
||||||
|
* If this is less than the maximum packet duration
|
||||||
|
* (120 ms; 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 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 samples decoded on success or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode(
|
||||||
|
OpusProjectionDecoder *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 a projection Opus packet with floating point output.
|
||||||
|
* @param st <tt>OpusProjectionDecoder*</tt>: Projection decoder state.
|
||||||
|
* @param[in] data <tt>const unsigned char*</tt>: Input payload.
|
||||||
|
* Use a <code>NULL</code>
|
||||||
|
* pointer to indicate packet
|
||||||
|
* loss.
|
||||||
|
* @param len <tt>opus_int32</tt>: Number of bytes in payload.
|
||||||
|
* @param[out] pcm <tt>opus_int16*</tt>: Output signal, with interleaved
|
||||||
|
* samples.
|
||||||
|
* This must contain room for
|
||||||
|
* <code>frame_size*channels</code>
|
||||||
|
* samples.
|
||||||
|
* @param frame_size <tt>int</tt>: The number of samples per channel of
|
||||||
|
* available space in \a pcm.
|
||||||
|
* If this is less than the maximum packet duration
|
||||||
|
* (120 ms; 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 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 samples decoded on success or a negative error code
|
||||||
|
* (see @ref opus_errorcodes) on failure.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_projection_decode_float(
|
||||||
|
OpusProjectionDecoder *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 a projection Opus decoder.
|
||||||
|
*
|
||||||
|
* Generally the request and subsequent arguments are generated by a
|
||||||
|
* convenience macro.
|
||||||
|
* @param st <tt>OpusProjectionDecoder*</tt>: Projection decoder state.
|
||||||
|
* @param request This and all remaining parameters should be replaced by one
|
||||||
|
* of the convenience macros in @ref opus_genericctls,
|
||||||
|
* @ref opus_decoderctls, @ref opus_multistream_ctls, or
|
||||||
|
* @ref opus_projection_ctls.
|
||||||
|
* @see opus_genericctls
|
||||||
|
* @see opus_decoderctls
|
||||||
|
* @see opus_multistream_ctls
|
||||||
|
* @see opus_projection_ctls
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT int opus_projection_decoder_ctl(OpusProjectionDecoder *st, int request, ...) OPUS_ARG_NONNULL(1);
|
||||||
|
|
||||||
|
|
||||||
|
/** Frees an <code>OpusProjectionDecoder</code> allocated by
|
||||||
|
* opus_projection_decoder_create().
|
||||||
|
* @param st <tt>OpusProjectionDecoder</tt>: Projection decoder state to be freed.
|
||||||
|
*/
|
||||||
|
OPUS_EXPORT void opus_projection_decoder_destroy(OpusProjectionDecoder *st);
|
||||||
|
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
/**@}*/
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OPUS_PROJECTION_H */
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */
|
||||||
|
/* Modified by Jean-Marc Valin */
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
/* opus_types.h based on ogg_types.h from libogg */
|
||||||
|
|
||||||
|
/**
|
||||||
|
@file opus_types.h
|
||||||
|
@brief Opus reference implementation types
|
||||||
|
*/
|
||||||
|
#ifndef OPUS_TYPES_H
|
||||||
|
#define OPUS_TYPES_H
|
||||||
|
|
||||||
|
#define opus_int int /* used for counters etc; at least 16 bits */
|
||||||
|
#define opus_int64 long long
|
||||||
|
#define opus_int8 signed char
|
||||||
|
|
||||||
|
#define opus_uint unsigned int /* used for counters etc; at least 16 bits */
|
||||||
|
#define opus_uint64 unsigned long long
|
||||||
|
#define opus_uint8 unsigned char
|
||||||
|
|
||||||
|
/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */
|
||||||
|
#if (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H))
|
||||||
|
#include <stdint.h>
|
||||||
|
# undef opus_int64
|
||||||
|
# undef opus_int8
|
||||||
|
# undef opus_uint64
|
||||||
|
# undef opus_uint8
|
||||||
|
typedef int8_t opus_int8;
|
||||||
|
typedef uint8_t opus_uint8;
|
||||||
|
typedef int16_t opus_int16;
|
||||||
|
typedef uint16_t opus_uint16;
|
||||||
|
typedef int32_t opus_int32;
|
||||||
|
typedef uint32_t opus_uint32;
|
||||||
|
typedef int64_t opus_int64;
|
||||||
|
typedef uint64_t opus_uint64;
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
|
||||||
|
# if defined(__CYGWIN__)
|
||||||
|
# include <_G_config.h>
|
||||||
|
typedef _G_int32_t opus_int32;
|
||||||
|
typedef _G_uint32_t opus_uint32;
|
||||||
|
typedef _G_int16 opus_int16;
|
||||||
|
typedef _G_uint16 opus_uint16;
|
||||||
|
# elif defined(__MINGW32__)
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
# elif defined(__MWERKS__)
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
# else
|
||||||
|
/* MSVC/Borland */
|
||||||
|
typedef __int32 opus_int32;
|
||||||
|
typedef unsigned __int32 opus_uint32;
|
||||||
|
typedef __int16 opus_int16;
|
||||||
|
typedef unsigned __int16 opus_uint16;
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#elif defined(__MACOS__)
|
||||||
|
|
||||||
|
# include <sys/types.h>
|
||||||
|
typedef SInt16 opus_int16;
|
||||||
|
typedef UInt16 opus_uint16;
|
||||||
|
typedef SInt32 opus_int32;
|
||||||
|
typedef UInt32 opus_uint32;
|
||||||
|
|
||||||
|
#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */
|
||||||
|
|
||||||
|
# include <sys/types.h>
|
||||||
|
typedef int16_t opus_int16;
|
||||||
|
typedef u_int16_t opus_uint16;
|
||||||
|
typedef int32_t opus_int32;
|
||||||
|
typedef u_int32_t opus_uint32;
|
||||||
|
|
||||||
|
#elif defined(__BEOS__)
|
||||||
|
|
||||||
|
/* Be */
|
||||||
|
# include <inttypes.h>
|
||||||
|
typedef int16 opus_int16;
|
||||||
|
typedef u_int16 opus_uint16;
|
||||||
|
typedef int32_t opus_int32;
|
||||||
|
typedef u_int32_t opus_uint32;
|
||||||
|
|
||||||
|
#elif defined (__EMX__)
|
||||||
|
|
||||||
|
/* OS/2 GCC */
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
|
||||||
|
#elif defined (DJGPP)
|
||||||
|
|
||||||
|
/* DJGPP */
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
|
||||||
|
#elif defined(R5900)
|
||||||
|
|
||||||
|
/* PS2 EE */
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned opus_uint32;
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
|
||||||
|
#elif defined(__SYMBIAN32__)
|
||||||
|
|
||||||
|
/* Symbian GCC */
|
||||||
|
typedef signed short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef signed int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
|
||||||
|
#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X)
|
||||||
|
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef long opus_int32;
|
||||||
|
typedef unsigned long opus_uint32;
|
||||||
|
|
||||||
|
#elif defined(CONFIG_TI_C6X)
|
||||||
|
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
/* Give up, take a reasonable guess */
|
||||||
|
typedef short opus_int16;
|
||||||
|
typedef unsigned short opus_uint16;
|
||||||
|
typedef int opus_int32;
|
||||||
|
typedef unsigned int opus_uint32;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OPUS_TYPES_H */
|
||||||
Binary file not shown.
Binary file not shown.
+1
Submodule app/src/main/jni/moonlight-core/moonlight-common-c added at 438b4f87d3
@@ -0,0 +1,13 @@
|
|||||||
|
LOCAL_PATH:= $(call my-dir)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libssl
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/libssl.a
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_STATIC_LIBRARY)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libcrypto
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/libcrypto.a
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_STATIC_LIBRARY)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
|
* in the file LICENSE in the source distribution or at
|
||||||
|
* https://www.openssl.org/source/license.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is only used by HP C on VMS, and is included automatically
|
||||||
|
* after each header file from this directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* restore state. Must correspond to the save in __decc_include_prologue.h */
|
||||||
|
#pragma names restore
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
|
* in the file LICENSE in the source distribution or at
|
||||||
|
* https://www.openssl.org/source/license.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is only used by HP C on VMS, and is included automatically
|
||||||
|
* after each header file from this directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* save state */
|
||||||
|
#pragma names save
|
||||||
|
/* have the compiler shorten symbols larger than 31 chars to 23 chars
|
||||||
|
* followed by a 8 hex char CRC
|
||||||
|
*/
|
||||||
|
#pragma names as_is,shortened
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2016 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
|
* in the file LICENSE in the source distribution or at
|
||||||
|
* https://www.openssl.org/source/license.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef HEADER_AES_H
|
||||||
|
# define HEADER_AES_H
|
||||||
|
|
||||||
|
# include <openssl/opensslconf.h>
|
||||||
|
|
||||||
|
# include <stddef.h>
|
||||||
|
# ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# define AES_ENCRYPT 1
|
||||||
|
# define AES_DECRYPT 0
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Because array size can't be a const in C, the following two are macros.
|
||||||
|
* Both sizes are in bytes.
|
||||||
|
*/
|
||||||
|
# define AES_MAXNR 14
|
||||||
|
# define AES_BLOCK_SIZE 16
|
||||||
|
|
||||||
|
/* This should be a hidden type, but EVP requires that the size be known */
|
||||||
|
struct aes_key_st {
|
||||||
|
# ifdef AES_LONG
|
||||||
|
unsigned long rd_key[4 * (AES_MAXNR + 1)];
|
||||||
|
# else
|
||||||
|
unsigned int rd_key[4 * (AES_MAXNR + 1)];
|
||||||
|
# endif
|
||||||
|
int rounds;
|
||||||
|
};
|
||||||
|
typedef struct aes_key_st AES_KEY;
|
||||||
|
|
||||||
|
const char *AES_options(void);
|
||||||
|
|
||||||
|
int AES_set_encrypt_key(const unsigned char *userKey, const int bits,
|
||||||
|
AES_KEY *key);
|
||||||
|
int AES_set_decrypt_key(const unsigned char *userKey, const int bits,
|
||||||
|
AES_KEY *key);
|
||||||
|
|
||||||
|
void AES_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
const AES_KEY *key);
|
||||||
|
void AES_decrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
const AES_KEY *key);
|
||||||
|
|
||||||
|
void AES_ecb_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
const AES_KEY *key, const int enc);
|
||||||
|
void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
unsigned char *ivec, const int enc);
|
||||||
|
void AES_cfb128_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
unsigned char *ivec, int *num, const int enc);
|
||||||
|
void AES_cfb1_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
unsigned char *ivec, int *num, const int enc);
|
||||||
|
void AES_cfb8_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
unsigned char *ivec, int *num, const int enc);
|
||||||
|
void AES_ofb128_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
unsigned char *ivec, int *num);
|
||||||
|
/* NB: the IV is _two_ blocks long */
|
||||||
|
void AES_ige_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
unsigned char *ivec, const int enc);
|
||||||
|
/* NB: the IV is _four_ blocks long */
|
||||||
|
void AES_bi_ige_encrypt(const unsigned char *in, unsigned char *out,
|
||||||
|
size_t length, const AES_KEY *key,
|
||||||
|
const AES_KEY *key2, const unsigned char *ivec,
|
||||||
|
const int enc);
|
||||||
|
|
||||||
|
int AES_wrap_key(AES_KEY *key, const unsigned char *iv,
|
||||||
|
unsigned char *out,
|
||||||
|
const unsigned char *in, unsigned int inlen);
|
||||||
|
int AES_unwrap_key(AES_KEY *key, const unsigned char *iv,
|
||||||
|
unsigned char *out,
|
||||||
|
const unsigned char *in, unsigned int inlen);
|
||||||
|
|
||||||
|
|
||||||
|
# ifdef __cplusplus
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,886 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 1995-2017 The OpenSSL Project Authors. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the OpenSSL license (the "License"). You may not use
|
||||||
|
* this file except in compliance with the License. You can obtain a copy
|
||||||
|
* in the file LICENSE in the source distribution or at
|
||||||
|
* https://www.openssl.org/source/license.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef HEADER_ASN1_H
|
||||||
|
# define HEADER_ASN1_H
|
||||||
|
|
||||||
|
# include <time.h>
|
||||||
|
# include <openssl/e_os2.h>
|
||||||
|
# include <openssl/opensslconf.h>
|
||||||
|
# include <openssl/bio.h>
|
||||||
|
# include <openssl/safestack.h>
|
||||||
|
# include <openssl/asn1err.h>
|
||||||
|
# include <openssl/symhacks.h>
|
||||||
|
|
||||||
|
# include <openssl/ossl_typ.h>
|
||||||
|
# if OPENSSL_API_COMPAT < 0x10100000L
|
||||||
|
# include <openssl/bn.h>
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# ifdef OPENSSL_BUILD_SHLIBCRYPTO
|
||||||
|
# undef OPENSSL_EXTERN
|
||||||
|
# define OPENSSL_EXTERN OPENSSL_EXPORT
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
# define V_ASN1_UNIVERSAL 0x00
|
||||||
|
# define V_ASN1_APPLICATION 0x40
|
||||||
|
# define V_ASN1_CONTEXT_SPECIFIC 0x80
|
||||||
|
# define V_ASN1_PRIVATE 0xc0
|
||||||
|
|
||||||
|
# define V_ASN1_CONSTRUCTED 0x20
|
||||||
|
# define V_ASN1_PRIMITIVE_TAG 0x1f
|
||||||
|
# define V_ASN1_PRIMATIVE_TAG /*compat*/ V_ASN1_PRIMITIVE_TAG
|
||||||
|
|
||||||
|
# define V_ASN1_APP_CHOOSE -2/* let the recipient choose */
|
||||||
|
# define V_ASN1_OTHER -3/* used in ASN1_TYPE */
|
||||||
|
# define V_ASN1_ANY -4/* used in ASN1 template code */
|
||||||
|
|
||||||
|
# define V_ASN1_UNDEF -1
|
||||||
|
/* ASN.1 tag values */
|
||||||
|
# define V_ASN1_EOC 0
|
||||||
|
# define V_ASN1_BOOLEAN 1 /**/
|
||||||
|
# define V_ASN1_INTEGER 2
|
||||||
|
# define V_ASN1_BIT_STRING 3
|
||||||
|
# define V_ASN1_OCTET_STRING 4
|
||||||
|
# define V_ASN1_NULL 5
|
||||||
|
# define V_ASN1_OBJECT 6
|
||||||
|
# define V_ASN1_OBJECT_DESCRIPTOR 7
|
||||||
|
# define V_ASN1_EXTERNAL 8
|
||||||
|
# define V_ASN1_REAL 9
|
||||||
|
# define V_ASN1_ENUMERATED 10
|
||||||
|
# define V_ASN1_UTF8STRING 12
|
||||||
|
# define V_ASN1_SEQUENCE 16
|
||||||
|
# define V_ASN1_SET 17
|
||||||
|
# define V_ASN1_NUMERICSTRING 18 /**/
|
||||||
|
# define V_ASN1_PRINTABLESTRING 19
|
||||||
|
# define V_ASN1_T61STRING 20
|
||||||
|
# define V_ASN1_TELETEXSTRING 20/* alias */
|
||||||
|
# define V_ASN1_VIDEOTEXSTRING 21 /**/
|
||||||
|
# define V_ASN1_IA5STRING 22
|
||||||
|
# define V_ASN1_UTCTIME 23
|
||||||
|
# define V_ASN1_GENERALIZEDTIME 24 /**/
|
||||||
|
# define V_ASN1_GRAPHICSTRING 25 /**/
|
||||||
|
# define V_ASN1_ISO64STRING 26 /**/
|
||||||
|
# define V_ASN1_VISIBLESTRING 26/* alias */
|
||||||
|
# define V_ASN1_GENERALSTRING 27 /**/
|
||||||
|
# define V_ASN1_UNIVERSALSTRING 28 /**/
|
||||||
|
# define V_ASN1_BMPSTRING 30
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NB the constants below are used internally by ASN1_INTEGER
|
||||||
|
* and ASN1_ENUMERATED to indicate the sign. They are *not* on
|
||||||
|
* the wire tag values.
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define V_ASN1_NEG 0x100
|
||||||
|
# define V_ASN1_NEG_INTEGER (2 | V_ASN1_NEG)
|
||||||
|
# define V_ASN1_NEG_ENUMERATED (10 | V_ASN1_NEG)
|
||||||
|
|
||||||
|
/* For use with d2i_ASN1_type_bytes() */
|
||||||
|
# define B_ASN1_NUMERICSTRING 0x0001
|
||||||
|
# define B_ASN1_PRINTABLESTRING 0x0002
|
||||||
|
# define B_ASN1_T61STRING 0x0004
|
||||||
|
# define B_ASN1_TELETEXSTRING 0x0004
|
||||||
|
# define B_ASN1_VIDEOTEXSTRING 0x0008
|
||||||
|
# define B_ASN1_IA5STRING 0x0010
|
||||||
|
# define B_ASN1_GRAPHICSTRING 0x0020
|
||||||
|
# define B_ASN1_ISO64STRING 0x0040
|
||||||
|
# define B_ASN1_VISIBLESTRING 0x0040
|
||||||
|
# define B_ASN1_GENERALSTRING 0x0080
|
||||||
|
# define B_ASN1_UNIVERSALSTRING 0x0100
|
||||||
|
# define B_ASN1_OCTET_STRING 0x0200
|
||||||
|
# define B_ASN1_BIT_STRING 0x0400
|
||||||
|
# define B_ASN1_BMPSTRING 0x0800
|
||||||
|
# define B_ASN1_UNKNOWN 0x1000
|
||||||
|
# define B_ASN1_UTF8STRING 0x2000
|
||||||
|
# define B_ASN1_UTCTIME 0x4000
|
||||||
|
# define B_ASN1_GENERALIZEDTIME 0x8000
|
||||||
|
# define B_ASN1_SEQUENCE 0x10000
|
||||||
|
/* For use with ASN1_mbstring_copy() */
|
||||||
|
# define MBSTRING_FLAG 0x1000
|
||||||
|
# define MBSTRING_UTF8 (MBSTRING_FLAG)
|
||||||
|
# define MBSTRING_ASC (MBSTRING_FLAG|1)
|
||||||
|
# define MBSTRING_BMP (MBSTRING_FLAG|2)
|
||||||
|
# define MBSTRING_UNIV (MBSTRING_FLAG|4)
|
||||||
|
# define SMIME_OLDMIME 0x400
|
||||||
|
# define SMIME_CRLFEOL 0x800
|
||||||
|
# define SMIME_STREAM 0x1000
|
||||||
|
struct X509_algor_st;
|
||||||
|
DEFINE_STACK_OF(X509_ALGOR)
|
||||||
|
|
||||||
|
# define ASN1_STRING_FLAG_BITS_LEFT 0x08/* Set if 0x07 has bits left value */
|
||||||
|
/*
|
||||||
|
* This indicates that the ASN1_STRING is not a real value but just a place
|
||||||
|
* holder for the location where indefinite length constructed data should be
|
||||||
|
* inserted in the memory buffer
|
||||||
|
*/
|
||||||
|
# define ASN1_STRING_FLAG_NDEF 0x010
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This flag is used by the CMS code to indicate that a string is not
|
||||||
|
* complete and is a place holder for content when it had all been accessed.
|
||||||
|
* The flag will be reset when content has been written to it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRING_FLAG_CONT 0x020
|
||||||
|
/*
|
||||||
|
* This flag is used by ASN1 code to indicate an ASN1_STRING is an MSTRING
|
||||||
|
* type.
|
||||||
|
*/
|
||||||
|
# define ASN1_STRING_FLAG_MSTRING 0x040
|
||||||
|
/* String is embedded and only content should be freed */
|
||||||
|
# define ASN1_STRING_FLAG_EMBED 0x080
|
||||||
|
/* String should be parsed in RFC 5280's time format */
|
||||||
|
# define ASN1_STRING_FLAG_X509_TIME 0x100
|
||||||
|
/* This is the base type that holds just about everything :-) */
|
||||||
|
struct asn1_string_st {
|
||||||
|
int length;
|
||||||
|
int type;
|
||||||
|
unsigned char *data;
|
||||||
|
/*
|
||||||
|
* The value of the following field depends on the type being held. It
|
||||||
|
* is mostly being used for BIT_STRING so if the input data has a
|
||||||
|
* non-zero 'unused bits' value, it will be handled correctly
|
||||||
|
*/
|
||||||
|
long flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ASN1_ENCODING structure: this is used to save the received encoding of an
|
||||||
|
* ASN1 type. This is useful to get round problems with invalid encodings
|
||||||
|
* which can break signatures.
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct ASN1_ENCODING_st {
|
||||||
|
unsigned char *enc; /* DER encoding */
|
||||||
|
long len; /* Length of encoding */
|
||||||
|
int modified; /* set to 1 if 'enc' is invalid */
|
||||||
|
} ASN1_ENCODING;
|
||||||
|
|
||||||
|
/* Used with ASN1 LONG type: if a long is set to this it is omitted */
|
||||||
|
# define ASN1_LONG_UNDEF 0x7fffffffL
|
||||||
|
|
||||||
|
# define STABLE_FLAGS_MALLOC 0x01
|
||||||
|
/*
|
||||||
|
* A zero passed to ASN1_STRING_TABLE_new_add for the flags is interpreted
|
||||||
|
* as "don't change" and STABLE_FLAGS_MALLOC is always set. By setting
|
||||||
|
* STABLE_FLAGS_MALLOC only we can clear the existing value. Use the alias
|
||||||
|
* STABLE_FLAGS_CLEAR to reflect this.
|
||||||
|
*/
|
||||||
|
# define STABLE_FLAGS_CLEAR STABLE_FLAGS_MALLOC
|
||||||
|
# define STABLE_NO_MASK 0x02
|
||||||
|
# define DIRSTRING_TYPE \
|
||||||
|
(B_ASN1_PRINTABLESTRING|B_ASN1_T61STRING|B_ASN1_BMPSTRING|B_ASN1_UTF8STRING)
|
||||||
|
# define PKCS9STRING_TYPE (DIRSTRING_TYPE|B_ASN1_IA5STRING)
|
||||||
|
|
||||||
|
typedef struct asn1_string_table_st {
|
||||||
|
int nid;
|
||||||
|
long minsize;
|
||||||
|
long maxsize;
|
||||||
|
unsigned long mask;
|
||||||
|
unsigned long flags;
|
||||||
|
} ASN1_STRING_TABLE;
|
||||||
|
|
||||||
|
DEFINE_STACK_OF(ASN1_STRING_TABLE)
|
||||||
|
|
||||||
|
/* size limits: this stuff is taken straight from RFC2459 */
|
||||||
|
|
||||||
|
# define ub_name 32768
|
||||||
|
# define ub_common_name 64
|
||||||
|
# define ub_locality_name 128
|
||||||
|
# define ub_state_name 128
|
||||||
|
# define ub_organization_name 64
|
||||||
|
# define ub_organization_unit_name 64
|
||||||
|
# define ub_title 64
|
||||||
|
# define ub_email_address 128
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Declarations for template structures: for full definitions see asn1t.h
|
||||||
|
*/
|
||||||
|
typedef struct ASN1_TEMPLATE_st ASN1_TEMPLATE;
|
||||||
|
typedef struct ASN1_TLC_st ASN1_TLC;
|
||||||
|
/* This is just an opaque pointer */
|
||||||
|
typedef struct ASN1_VALUE_st ASN1_VALUE;
|
||||||
|
|
||||||
|
/* Declare ASN1 functions: the implement macro in in asn1t.h */
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_FUNCTIONS(type) DECLARE_ASN1_FUNCTIONS_name(type, type)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_ALLOC_FUNCTIONS(type) \
|
||||||
|
DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, type)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_FUNCTIONS_name(type, name) \
|
||||||
|
DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, name) \
|
||||||
|
DECLARE_ASN1_ENCODE_FUNCTIONS(type, name, name)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_FUNCTIONS_fname(type, itname, name) \
|
||||||
|
DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, name) \
|
||||||
|
DECLARE_ASN1_ENCODE_FUNCTIONS(type, itname, name)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_ENCODE_FUNCTIONS(type, itname, name) \
|
||||||
|
type *d2i_##name(type **a, const unsigned char **in, long len); \
|
||||||
|
int i2d_##name(type *a, unsigned char **out); \
|
||||||
|
DECLARE_ASN1_ITEM(itname)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_ENCODE_FUNCTIONS_const(type, name) \
|
||||||
|
type *d2i_##name(type **a, const unsigned char **in, long len); \
|
||||||
|
int i2d_##name(const type *a, unsigned char **out); \
|
||||||
|
DECLARE_ASN1_ITEM(name)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_NDEF_FUNCTION(name) \
|
||||||
|
int i2d_##name##_NDEF(name *a, unsigned char **out);
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_FUNCTIONS_const(name) \
|
||||||
|
DECLARE_ASN1_ALLOC_FUNCTIONS(name) \
|
||||||
|
DECLARE_ASN1_ENCODE_FUNCTIONS_const(name, name)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_ALLOC_FUNCTIONS_name(type, name) \
|
||||||
|
type *name##_new(void); \
|
||||||
|
void name##_free(type *a);
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_PRINT_FUNCTION(stname) \
|
||||||
|
DECLARE_ASN1_PRINT_FUNCTION_fname(stname, stname)
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_PRINT_FUNCTION_fname(stname, fname) \
|
||||||
|
int fname##_print_ctx(BIO *out, stname *x, int indent, \
|
||||||
|
const ASN1_PCTX *pctx);
|
||||||
|
|
||||||
|
# define D2I_OF(type) type *(*)(type **,const unsigned char **,long)
|
||||||
|
# define I2D_OF(type) int (*)(type *,unsigned char **)
|
||||||
|
# define I2D_OF_const(type) int (*)(const type *,unsigned char **)
|
||||||
|
|
||||||
|
# define CHECKED_D2I_OF(type, d2i) \
|
||||||
|
((d2i_of_void*) (1 ? d2i : ((D2I_OF(type))0)))
|
||||||
|
# define CHECKED_I2D_OF(type, i2d) \
|
||||||
|
((i2d_of_void*) (1 ? i2d : ((I2D_OF(type))0)))
|
||||||
|
# define CHECKED_NEW_OF(type, xnew) \
|
||||||
|
((void *(*)(void)) (1 ? xnew : ((type *(*)(void))0)))
|
||||||
|
# define CHECKED_PTR_OF(type, p) \
|
||||||
|
((void*) (1 ? p : (type*)0))
|
||||||
|
# define CHECKED_PPTR_OF(type, p) \
|
||||||
|
((void**) (1 ? p : (type**)0))
|
||||||
|
|
||||||
|
# define TYPEDEF_D2I_OF(type) typedef type *d2i_of_##type(type **,const unsigned char **,long)
|
||||||
|
# define TYPEDEF_I2D_OF(type) typedef int i2d_of_##type(type *,unsigned char **)
|
||||||
|
# define TYPEDEF_D2I2D_OF(type) TYPEDEF_D2I_OF(type); TYPEDEF_I2D_OF(type)
|
||||||
|
|
||||||
|
TYPEDEF_D2I2D_OF(void);
|
||||||
|
|
||||||
|
/*-
|
||||||
|
* The following macros and typedefs allow an ASN1_ITEM
|
||||||
|
* to be embedded in a structure and referenced. Since
|
||||||
|
* the ASN1_ITEM pointers need to be globally accessible
|
||||||
|
* (possibly from shared libraries) they may exist in
|
||||||
|
* different forms. On platforms that support it the
|
||||||
|
* ASN1_ITEM structure itself will be globally exported.
|
||||||
|
* Other platforms will export a function that returns
|
||||||
|
* an ASN1_ITEM pointer.
|
||||||
|
*
|
||||||
|
* To handle both cases transparently the macros below
|
||||||
|
* should be used instead of hard coding an ASN1_ITEM
|
||||||
|
* pointer in a structure.
|
||||||
|
*
|
||||||
|
* The structure will look like this:
|
||||||
|
*
|
||||||
|
* typedef struct SOMETHING_st {
|
||||||
|
* ...
|
||||||
|
* ASN1_ITEM_EXP *iptr;
|
||||||
|
* ...
|
||||||
|
* } SOMETHING;
|
||||||
|
*
|
||||||
|
* It would be initialised as e.g.:
|
||||||
|
*
|
||||||
|
* SOMETHING somevar = {...,ASN1_ITEM_ref(X509),...};
|
||||||
|
*
|
||||||
|
* and the actual pointer extracted with:
|
||||||
|
*
|
||||||
|
* const ASN1_ITEM *it = ASN1_ITEM_ptr(somevar.iptr);
|
||||||
|
*
|
||||||
|
* Finally an ASN1_ITEM pointer can be extracted from an
|
||||||
|
* appropriate reference with: ASN1_ITEM_rptr(X509). This
|
||||||
|
* would be used when a function takes an ASN1_ITEM * argument.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
# ifndef OPENSSL_EXPORT_VAR_AS_FUNCTION
|
||||||
|
|
||||||
|
/* ASN1_ITEM pointer exported type */
|
||||||
|
typedef const ASN1_ITEM ASN1_ITEM_EXP;
|
||||||
|
|
||||||
|
/* Macro to obtain ASN1_ITEM pointer from exported type */
|
||||||
|
# define ASN1_ITEM_ptr(iptr) (iptr)
|
||||||
|
|
||||||
|
/* Macro to include ASN1_ITEM pointer from base type */
|
||||||
|
# define ASN1_ITEM_ref(iptr) (&(iptr##_it))
|
||||||
|
|
||||||
|
# define ASN1_ITEM_rptr(ref) (&(ref##_it))
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_ITEM(name) \
|
||||||
|
OPENSSL_EXTERN const ASN1_ITEM name##_it;
|
||||||
|
|
||||||
|
# else
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Platforms that can't easily handle shared global variables are declared as
|
||||||
|
* functions returning ASN1_ITEM pointers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ASN1_ITEM pointer exported type */
|
||||||
|
typedef const ASN1_ITEM *ASN1_ITEM_EXP (void);
|
||||||
|
|
||||||
|
/* Macro to obtain ASN1_ITEM pointer from exported type */
|
||||||
|
# define ASN1_ITEM_ptr(iptr) (iptr())
|
||||||
|
|
||||||
|
/* Macro to include ASN1_ITEM pointer from base type */
|
||||||
|
# define ASN1_ITEM_ref(iptr) (iptr##_it)
|
||||||
|
|
||||||
|
# define ASN1_ITEM_rptr(ref) (ref##_it())
|
||||||
|
|
||||||
|
# define DECLARE_ASN1_ITEM(name) \
|
||||||
|
const ASN1_ITEM * name##_it(void);
|
||||||
|
|
||||||
|
# endif
|
||||||
|
|
||||||
|
/* Parameters used by ASN1_STRING_print_ex() */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These determine which characters to escape: RFC2253 special characters,
|
||||||
|
* control characters and MSB set characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_ESC_2253 1
|
||||||
|
# define ASN1_STRFLGS_ESC_CTRL 2
|
||||||
|
# define ASN1_STRFLGS_ESC_MSB 4
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This flag determines how we do escaping: normally RC2253 backslash only,
|
||||||
|
* set this to use backslash and quote.
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_ESC_QUOTE 8
|
||||||
|
|
||||||
|
/* These three flags are internal use only. */
|
||||||
|
|
||||||
|
/* Character is a valid PrintableString character */
|
||||||
|
# define CHARTYPE_PRINTABLESTRING 0x10
|
||||||
|
/* Character needs escaping if it is the first character */
|
||||||
|
# define CHARTYPE_FIRST_ESC_2253 0x20
|
||||||
|
/* Character needs escaping if it is the last character */
|
||||||
|
# define CHARTYPE_LAST_ESC_2253 0x40
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NB the internal flags are safely reused below by flags handled at the top
|
||||||
|
* level.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If this is set we convert all character strings to UTF8 first
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_UTF8_CONVERT 0x10
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If this is set we don't attempt to interpret content: just assume all
|
||||||
|
* strings are 1 byte per character. This will produce some pretty odd
|
||||||
|
* looking output!
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_IGNORE_TYPE 0x20
|
||||||
|
|
||||||
|
/* If this is set we include the string type in the output */
|
||||||
|
# define ASN1_STRFLGS_SHOW_TYPE 0x40
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This determines which strings to display and which to 'dump' (hex dump of
|
||||||
|
* content octets or DER encoding). We can only dump non character strings or
|
||||||
|
* everything. If we don't dump 'unknown' they are interpreted as character
|
||||||
|
* strings with 1 octet per character and are subject to the usual escaping
|
||||||
|
* options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_DUMP_ALL 0x80
|
||||||
|
# define ASN1_STRFLGS_DUMP_UNKNOWN 0x100
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These determine what 'dumping' does, we can dump the content octets or the
|
||||||
|
* DER encoding: both use the RFC2253 #XXXXX notation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_DUMP_DER 0x200
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This flag specifies that RC2254 escaping shall be performed.
|
||||||
|
*/
|
||||||
|
#define ASN1_STRFLGS_ESC_2254 0x400
|
||||||
|
|
||||||
|
/*
|
||||||
|
* All the string flags consistent with RFC2253, escaping control characters
|
||||||
|
* isn't essential in RFC2253 but it is advisable anyway.
|
||||||
|
*/
|
||||||
|
|
||||||
|
# define ASN1_STRFLGS_RFC2253 (ASN1_STRFLGS_ESC_2253 | \
|
||||||
|
ASN1_STRFLGS_ESC_CTRL | \
|
||||||
|
ASN1_STRFLGS_ESC_MSB | \
|
||||||
|
ASN1_STRFLGS_UTF8_CONVERT | \
|
||||||
|
ASN1_STRFLGS_DUMP_UNKNOWN | \
|
||||||
|
ASN1_STRFLGS_DUMP_DER)
|
||||||
|
|
||||||
|
DEFINE_STACK_OF(ASN1_INTEGER)
|
||||||
|
|
||||||
|
DEFINE_STACK_OF(ASN1_GENERALSTRING)
|
||||||
|
|
||||||
|
DEFINE_STACK_OF(ASN1_UTF8STRING)
|
||||||
|
|
||||||
|
typedef struct asn1_type_st {
|
||||||
|
int type;
|
||||||
|
union {
|
||||||
|
char *ptr;
|
||||||
|
ASN1_BOOLEAN boolean;
|
||||||
|
ASN1_STRING *asn1_string;
|
||||||
|
ASN1_OBJECT *object;
|
||||||
|
ASN1_INTEGER *integer;
|
||||||
|
ASN1_ENUMERATED *enumerated;
|
||||||
|
ASN1_BIT_STRING *bit_string;
|
||||||
|
ASN1_OCTET_STRING *octet_string;
|
||||||
|
ASN1_PRINTABLESTRING *printablestring;
|
||||||
|
ASN1_T61STRING *t61string;
|
||||||
|
ASN1_IA5STRING *ia5string;
|
||||||
|
ASN1_GENERALSTRING *generalstring;
|
||||||
|
ASN1_BMPSTRING *bmpstring;
|
||||||
|
ASN1_UNIVERSALSTRING *universalstring;
|
||||||
|
ASN1_UTCTIME *utctime;
|
||||||
|
ASN1_GENERALIZEDTIME *generalizedtime;
|
||||||
|
ASN1_VISIBLESTRING *visiblestring;
|
||||||
|
ASN1_UTF8STRING *utf8string;
|
||||||
|
/*
|
||||||
|
* set and sequence are left complete and still contain the set or
|
||||||
|
* sequence bytes
|
||||||
|
*/
|
||||||
|
ASN1_STRING *set;
|
||||||
|
ASN1_STRING *sequence;
|
||||||
|
ASN1_VALUE *asn1_value;
|
||||||
|
} value;
|
||||||
|
} ASN1_TYPE;
|
||||||
|
|
||||||
|
DEFINE_STACK_OF(ASN1_TYPE)
|
||||||
|
|
||||||
|
typedef STACK_OF(ASN1_TYPE) ASN1_SEQUENCE_ANY;
|
||||||
|
|
||||||
|
DECLARE_ASN1_ENCODE_FUNCTIONS_const(ASN1_SEQUENCE_ANY, ASN1_SEQUENCE_ANY)
|
||||||
|
DECLARE_ASN1_ENCODE_FUNCTIONS_const(ASN1_SEQUENCE_ANY, ASN1_SET_ANY)
|
||||||
|
|
||||||
|
/* This is used to contain a list of bit names */
|
||||||
|
typedef struct BIT_STRING_BITNAME_st {
|
||||||
|
int bitnum;
|
||||||
|
const char *lname;
|
||||||
|
const char *sname;
|
||||||
|
} BIT_STRING_BITNAME;
|
||||||
|
|
||||||
|
# define B_ASN1_TIME \
|
||||||
|
B_ASN1_UTCTIME | \
|
||||||
|
B_ASN1_GENERALIZEDTIME
|
||||||
|
|
||||||
|
# define B_ASN1_PRINTABLE \
|
||||||
|
B_ASN1_NUMERICSTRING| \
|
||||||
|
B_ASN1_PRINTABLESTRING| \
|
||||||
|
B_ASN1_T61STRING| \
|
||||||
|
B_ASN1_IA5STRING| \
|
||||||
|
B_ASN1_BIT_STRING| \
|
||||||
|
B_ASN1_UNIVERSALSTRING|\
|
||||||
|
B_ASN1_BMPSTRING|\
|
||||||
|
B_ASN1_UTF8STRING|\
|
||||||
|
B_ASN1_SEQUENCE|\
|
||||||
|
B_ASN1_UNKNOWN
|
||||||
|
|
||||||
|
# define B_ASN1_DIRECTORYSTRING \
|
||||||
|
B_ASN1_PRINTABLESTRING| \
|
||||||
|
B_ASN1_TELETEXSTRING|\
|
||||||
|
B_ASN1_BMPSTRING|\
|
||||||
|
B_ASN1_UNIVERSALSTRING|\
|
||||||
|
B_ASN1_UTF8STRING
|
||||||
|
|
||||||
|
# define B_ASN1_DISPLAYTEXT \
|
||||||
|
B_ASN1_IA5STRING| \
|
||||||
|
B_ASN1_VISIBLESTRING| \
|
||||||
|
B_ASN1_BMPSTRING|\
|
||||||
|
B_ASN1_UTF8STRING
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS_fname(ASN1_TYPE, ASN1_ANY, ASN1_TYPE)
|
||||||
|
|
||||||
|
int ASN1_TYPE_get(const ASN1_TYPE *a);
|
||||||
|
void ASN1_TYPE_set(ASN1_TYPE *a, int type, void *value);
|
||||||
|
int ASN1_TYPE_set1(ASN1_TYPE *a, int type, const void *value);
|
||||||
|
int ASN1_TYPE_cmp(const ASN1_TYPE *a, const ASN1_TYPE *b);
|
||||||
|
|
||||||
|
ASN1_TYPE *ASN1_TYPE_pack_sequence(const ASN1_ITEM *it, void *s, ASN1_TYPE **t);
|
||||||
|
void *ASN1_TYPE_unpack_sequence(const ASN1_ITEM *it, const ASN1_TYPE *t);
|
||||||
|
|
||||||
|
ASN1_OBJECT *ASN1_OBJECT_new(void);
|
||||||
|
void ASN1_OBJECT_free(ASN1_OBJECT *a);
|
||||||
|
int i2d_ASN1_OBJECT(const ASN1_OBJECT *a, unsigned char **pp);
|
||||||
|
ASN1_OBJECT *d2i_ASN1_OBJECT(ASN1_OBJECT **a, const unsigned char **pp,
|
||||||
|
long length);
|
||||||
|
|
||||||
|
DECLARE_ASN1_ITEM(ASN1_OBJECT)
|
||||||
|
|
||||||
|
DEFINE_STACK_OF(ASN1_OBJECT)
|
||||||
|
|
||||||
|
ASN1_STRING *ASN1_STRING_new(void);
|
||||||
|
void ASN1_STRING_free(ASN1_STRING *a);
|
||||||
|
void ASN1_STRING_clear_free(ASN1_STRING *a);
|
||||||
|
int ASN1_STRING_copy(ASN1_STRING *dst, const ASN1_STRING *str);
|
||||||
|
ASN1_STRING *ASN1_STRING_dup(const ASN1_STRING *a);
|
||||||
|
ASN1_STRING *ASN1_STRING_type_new(int type);
|
||||||
|
int ASN1_STRING_cmp(const ASN1_STRING *a, const ASN1_STRING *b);
|
||||||
|
/*
|
||||||
|
* Since this is used to store all sorts of things, via macros, for now,
|
||||||
|
* make its data void *
|
||||||
|
*/
|
||||||
|
int ASN1_STRING_set(ASN1_STRING *str, const void *data, int len);
|
||||||
|
void ASN1_STRING_set0(ASN1_STRING *str, void *data, int len);
|
||||||
|
int ASN1_STRING_length(const ASN1_STRING *x);
|
||||||
|
void ASN1_STRING_length_set(ASN1_STRING *x, int n);
|
||||||
|
int ASN1_STRING_type(const ASN1_STRING *x);
|
||||||
|
DEPRECATEDIN_1_1_0(unsigned char *ASN1_STRING_data(ASN1_STRING *x))
|
||||||
|
const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *x);
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_BIT_STRING)
|
||||||
|
int ASN1_BIT_STRING_set(ASN1_BIT_STRING *a, unsigned char *d, int length);
|
||||||
|
int ASN1_BIT_STRING_set_bit(ASN1_BIT_STRING *a, int n, int value);
|
||||||
|
int ASN1_BIT_STRING_get_bit(const ASN1_BIT_STRING *a, int n);
|
||||||
|
int ASN1_BIT_STRING_check(const ASN1_BIT_STRING *a,
|
||||||
|
const unsigned char *flags, int flags_len);
|
||||||
|
|
||||||
|
int ASN1_BIT_STRING_name_print(BIO *out, ASN1_BIT_STRING *bs,
|
||||||
|
BIT_STRING_BITNAME *tbl, int indent);
|
||||||
|
int ASN1_BIT_STRING_num_asc(const char *name, BIT_STRING_BITNAME *tbl);
|
||||||
|
int ASN1_BIT_STRING_set_asc(ASN1_BIT_STRING *bs, const char *name, int value,
|
||||||
|
BIT_STRING_BITNAME *tbl);
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_INTEGER)
|
||||||
|
ASN1_INTEGER *d2i_ASN1_UINTEGER(ASN1_INTEGER **a, const unsigned char **pp,
|
||||||
|
long length);
|
||||||
|
ASN1_INTEGER *ASN1_INTEGER_dup(const ASN1_INTEGER *x);
|
||||||
|
int ASN1_INTEGER_cmp(const ASN1_INTEGER *x, const ASN1_INTEGER *y);
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_ENUMERATED)
|
||||||
|
|
||||||
|
int ASN1_UTCTIME_check(const ASN1_UTCTIME *a);
|
||||||
|
ASN1_UTCTIME *ASN1_UTCTIME_set(ASN1_UTCTIME *s, time_t t);
|
||||||
|
ASN1_UTCTIME *ASN1_UTCTIME_adj(ASN1_UTCTIME *s, time_t t,
|
||||||
|
int offset_day, long offset_sec);
|
||||||
|
int ASN1_UTCTIME_set_string(ASN1_UTCTIME *s, const char *str);
|
||||||
|
int ASN1_UTCTIME_cmp_time_t(const ASN1_UTCTIME *s, time_t t);
|
||||||
|
|
||||||
|
int ASN1_GENERALIZEDTIME_check(const ASN1_GENERALIZEDTIME *a);
|
||||||
|
ASN1_GENERALIZEDTIME *ASN1_GENERALIZEDTIME_set(ASN1_GENERALIZEDTIME *s,
|
||||||
|
time_t t);
|
||||||
|
ASN1_GENERALIZEDTIME *ASN1_GENERALIZEDTIME_adj(ASN1_GENERALIZEDTIME *s,
|
||||||
|
time_t t, int offset_day,
|
||||||
|
long offset_sec);
|
||||||
|
int ASN1_GENERALIZEDTIME_set_string(ASN1_GENERALIZEDTIME *s, const char *str);
|
||||||
|
|
||||||
|
int ASN1_TIME_diff(int *pday, int *psec,
|
||||||
|
const ASN1_TIME *from, const ASN1_TIME *to);
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_OCTET_STRING)
|
||||||
|
ASN1_OCTET_STRING *ASN1_OCTET_STRING_dup(const ASN1_OCTET_STRING *a);
|
||||||
|
int ASN1_OCTET_STRING_cmp(const ASN1_OCTET_STRING *a,
|
||||||
|
const ASN1_OCTET_STRING *b);
|
||||||
|
int ASN1_OCTET_STRING_set(ASN1_OCTET_STRING *str, const unsigned char *data,
|
||||||
|
int len);
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_VISIBLESTRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_UNIVERSALSTRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_UTF8STRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_NULL)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_BMPSTRING)
|
||||||
|
|
||||||
|
int UTF8_getc(const unsigned char *str, int len, unsigned long *val);
|
||||||
|
int UTF8_putc(unsigned char *str, int len, unsigned long value);
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS_name(ASN1_STRING, ASN1_PRINTABLE)
|
||||||
|
|
||||||
|
DECLARE_ASN1_FUNCTIONS_name(ASN1_STRING, DIRECTORYSTRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS_name(ASN1_STRING, DISPLAYTEXT)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_PRINTABLESTRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_T61STRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_IA5STRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_GENERALSTRING)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_UTCTIME)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_GENERALIZEDTIME)
|
||||||
|
DECLARE_ASN1_FUNCTIONS(ASN1_TIME)
|
||||||
|
|
||||||
|
DECLARE_ASN1_ITEM(ASN1_OCTET_STRING_NDEF)
|
||||||
|
|
||||||
|
ASN1_TIME *ASN1_TIME_set(ASN1_TIME *s, time_t t);
|
||||||
|
ASN1_TIME *ASN1_TIME_adj(ASN1_TIME *s, time_t t,
|
||||||
|
int offset_day, long offset_sec);
|
||||||
|
int ASN1_TIME_check(const ASN1_TIME *t);
|
||||||
|
ASN1_GENERALIZEDTIME *ASN1_TIME_to_generalizedtime(const ASN1_TIME *t,
|
||||||
|
ASN1_GENERALIZEDTIME **out);
|
||||||
|
int ASN1_TIME_set_string(ASN1_TIME *s, const char *str);
|
||||||
|
int ASN1_TIME_set_string_X509(ASN1_TIME *s, const char *str);
|
||||||
|
int ASN1_TIME_to_tm(const ASN1_TIME *s, struct tm *tm);
|
||||||
|
int ASN1_TIME_normalize(ASN1_TIME *s);
|
||||||
|
int ASN1_TIME_cmp_time_t(const ASN1_TIME *s, time_t t);
|
||||||
|
int ASN1_TIME_compare(const ASN1_TIME *a, const ASN1_TIME *b);
|
||||||
|
|
||||||
|
int i2a_ASN1_INTEGER(BIO *bp, const ASN1_INTEGER *a);
|
||||||
|
int a2i_ASN1_INTEGER(BIO *bp, ASN1_INTEGER *bs, char *buf, int size);
|
||||||
|
int i2a_ASN1_ENUMERATED(BIO *bp, const ASN1_ENUMERATED *a);
|
||||||
|
int a2i_ASN1_ENUMERATED(BIO *bp, ASN1_ENUMERATED *bs, char *buf, int size);
|
||||||
|
int i2a_ASN1_OBJECT(BIO *bp, const ASN1_OBJECT *a);
|
||||||
|
int a2i_ASN1_STRING(BIO *bp, ASN1_STRING *bs, char *buf, int size);
|
||||||
|
int i2a_ASN1_STRING(BIO *bp, const ASN1_STRING *a, int type);
|
||||||
|
int i2t_ASN1_OBJECT(char *buf, int buf_len, const ASN1_OBJECT *a);
|
||||||
|
|
||||||
|
int a2d_ASN1_OBJECT(unsigned char *out, int olen, const char *buf, int num);
|
||||||
|
ASN1_OBJECT *ASN1_OBJECT_create(int nid, unsigned char *data, int len,
|
||||||
|
const char *sn, const char *ln);
|
||||||
|
|
||||||
|
int ASN1_INTEGER_get_int64(int64_t *pr, const ASN1_INTEGER *a);
|
||||||
|
int ASN1_INTEGER_set_int64(ASN1_INTEGER *a, int64_t r);
|
||||||
|
int ASN1_INTEGER_get_uint64(uint64_t *pr, const ASN1_INTEGER *a);
|
||||||
|
int ASN1_INTEGER_set_uint64(ASN1_INTEGER *a, uint64_t r);
|
||||||
|
|
||||||
|
int ASN1_INTEGER_set(ASN1_INTEGER *a, long v);
|
||||||
|
long ASN1_INTEGER_get(const ASN1_INTEGER *a);
|
||||||
|
ASN1_INTEGER *BN_to_ASN1_INTEGER(const BIGNUM *bn, ASN1_INTEGER *ai);
|
||||||
|
BIGNUM *ASN1_INTEGER_to_BN(const ASN1_INTEGER *ai, BIGNUM *bn);
|
||||||
|
|
||||||
|
int ASN1_ENUMERATED_get_int64(int64_t *pr, const ASN1_ENUMERATED *a);
|
||||||
|
int ASN1_ENUMERATED_set_int64(ASN1_ENUMERATED *a, int64_t r);
|
||||||
|
|
||||||
|
|
||||||
|
int ASN1_ENUMERATED_set(ASN1_ENUMERATED *a, long v);
|
||||||
|
long ASN1_ENUMERATED_get(const ASN1_ENUMERATED *a);
|
||||||
|
ASN1_ENUMERATED *BN_to_ASN1_ENUMERATED(const BIGNUM *bn, ASN1_ENUMERATED *ai);
|
||||||
|
BIGNUM *ASN1_ENUMERATED_to_BN(const ASN1_ENUMERATED *ai, BIGNUM *bn);
|
||||||
|
|
||||||
|
/* General */
|
||||||
|
/* given a string, return the correct type, max is the maximum length */
|
||||||
|
int ASN1_PRINTABLE_type(const unsigned char *s, int max);
|
||||||
|
|
||||||
|
unsigned long ASN1_tag2bit(int tag);
|
||||||
|
|
||||||
|
/* SPECIALS */
|
||||||
|
int ASN1_get_object(const unsigned char **pp, long *plength, int *ptag,
|
||||||
|
int *pclass, long omax);
|
||||||
|
int ASN1_check_infinite_end(unsigned char **p, long len);
|
||||||
|
int ASN1_const_check_infinite_end(const unsigned char **p, long len);
|
||||||
|
void ASN1_put_object(unsigned char **pp, int constructed, int length,
|
||||||
|
int tag, int xclass);
|
||||||
|
int ASN1_put_eoc(unsigned char **pp);
|
||||||
|
int ASN1_object_size(int constructed, int length, int tag);
|
||||||
|
|
||||||
|
/* Used to implement other functions */
|
||||||
|
void *ASN1_dup(i2d_of_void *i2d, d2i_of_void *d2i, void *x);
|
||||||
|
|
||||||
|
# define ASN1_dup_of(type,i2d,d2i,x) \
|
||||||
|
((type*)ASN1_dup(CHECKED_I2D_OF(type, i2d), \
|
||||||
|
CHECKED_D2I_OF(type, d2i), \
|
||||||
|
CHECKED_PTR_OF(type, x)))
|
||||||
|
|
||||||
|
# define ASN1_dup_of_const(type,i2d,d2i,x) \
|
||||||
|
((type*)ASN1_dup(CHECKED_I2D_OF(const type, i2d), \
|
||||||
|
CHECKED_D2I_OF(type, d2i), \
|
||||||
|
CHECKED_PTR_OF(const type, x)))
|
||||||
|
|
||||||
|
void *ASN1_item_dup(const ASN1_ITEM *it, void *x);
|
||||||
|
|
||||||
|
/* ASN1 alloc/free macros for when a type is only used internally */
|
||||||
|
|
||||||
|
# define M_ASN1_new_of(type) (type *)ASN1_item_new(ASN1_ITEM_rptr(type))
|
||||||
|
# define M_ASN1_free_of(x, type) \
|
||||||
|
ASN1_item_free(CHECKED_PTR_OF(type, x), ASN1_ITEM_rptr(type))
|
||||||
|
|
||||||
|
# ifndef OPENSSL_NO_STDIO
|
||||||
|
void *ASN1_d2i_fp(void *(*xnew) (void), d2i_of_void *d2i, FILE *in, void **x);
|
||||||
|
|
||||||
|
# define ASN1_d2i_fp_of(type,xnew,d2i,in,x) \
|
||||||
|
((type*)ASN1_d2i_fp(CHECKED_NEW_OF(type, xnew), \
|
||||||
|
CHECKED_D2I_OF(type, d2i), \
|
||||||
|
in, \
|
||||||
|
CHECKED_PPTR_OF(type, x)))
|
||||||
|
|
||||||
|
void *ASN1_item_d2i_fp(const ASN1_ITEM *it, FILE *in, void *x);
|
||||||
|
int ASN1_i2d_fp(i2d_of_void *i2d, FILE *out, void *x);
|
||||||
|
|
||||||
|
# define ASN1_i2d_fp_of(type,i2d,out,x) \
|
||||||
|
(ASN1_i2d_fp(CHECKED_I2D_OF(type, i2d), \
|
||||||
|
out, \
|
||||||
|
CHECKED_PTR_OF(type, x)))
|
||||||
|
|
||||||
|
# define ASN1_i2d_fp_of_const(type,i2d,out,x) \
|
||||||
|
(ASN1_i2d_fp(CHECKED_I2D_OF(const type, i2d), \
|
||||||
|
out, \
|
||||||
|
CHECKED_PTR_OF(const type, x)))
|
||||||
|
|
||||||
|
int ASN1_item_i2d_fp(const ASN1_ITEM *it, FILE *out, void *x);
|
||||||
|
int ASN1_STRING_print_ex_fp(FILE *fp, const ASN1_STRING *str, unsigned long flags);
|
||||||
|
# endif
|
||||||
|
|
||||||
|
int ASN1_STRING_to_UTF8(unsigned char **out, const ASN1_STRING *in);
|
||||||
|
|
||||||
|
void *ASN1_d2i_bio(void *(*xnew) (void), d2i_of_void *d2i, BIO *in, void **x);
|
||||||
|
|
||||||
|
# define ASN1_d2i_bio_of(type,xnew,d2i,in,x) \
|
||||||
|
((type*)ASN1_d2i_bio( CHECKED_NEW_OF(type, xnew), \
|
||||||
|
CHECKED_D2I_OF(type, d2i), \
|
||||||
|
in, \
|
||||||
|
CHECKED_PPTR_OF(type, x)))
|
||||||
|
|
||||||
|
void *ASN1_item_d2i_bio(const ASN1_ITEM *it, BIO *in, void *x);
|
||||||
|
int ASN1_i2d_bio(i2d_of_void *i2d, BIO *out, unsigned char *x);
|
||||||
|
|
||||||
|
# define ASN1_i2d_bio_of(type,i2d,out,x) \
|
||||||
|
(ASN1_i2d_bio(CHECKED_I2D_OF(type, i2d), \
|
||||||
|
out, \
|
||||||
|
CHECKED_PTR_OF(type, x)))
|
||||||
|
|
||||||
|
# define ASN1_i2d_bio_of_const(type,i2d,out,x) \
|
||||||
|
(ASN1_i2d_bio(CHECKED_I2D_OF(const type, i2d), \
|
||||||
|
out, \
|
||||||
|
CHECKED_PTR_OF(const type, x)))
|
||||||
|
|
||||||
|
int ASN1_item_i2d_bio(const ASN1_ITEM *it, BIO *out, void *x);
|
||||||
|
int ASN1_UTCTIME_print(BIO *fp, const ASN1_UTCTIME *a);
|
||||||
|
int ASN1_GENERALIZEDTIME_print(BIO *fp, const ASN1_GENERALIZEDTIME *a);
|
||||||
|
int ASN1_TIME_print(BIO *fp, const ASN1_TIME *a);
|
||||||
|
int ASN1_STRING_print(BIO *bp, const ASN1_STRING *v);
|
||||||
|
int ASN1_STRING_print_ex(BIO *out, const ASN1_STRING *str, unsigned long flags);
|
||||||
|
int ASN1_buf_print(BIO *bp, const unsigned char *buf, size_t buflen, int off);
|
||||||
|
int ASN1_bn_print(BIO *bp, const char *number, const BIGNUM *num,
|
||||||
|
unsigned char *buf, int off);
|
||||||
|
int ASN1_parse(BIO *bp, const unsigned char *pp, long len, int indent);
|
||||||
|
int ASN1_parse_dump(BIO *bp, const unsigned char *pp, long len, int indent,
|
||||||
|
int dump);
|
||||||
|
const char *ASN1_tag2str(int tag);
|
||||||
|
|
||||||
|
/* Used to load and write Netscape format cert */
|
||||||
|
|
||||||
|
int ASN1_UNIVERSALSTRING_to_string(ASN1_UNIVERSALSTRING *s);
|
||||||
|
|
||||||
|
int ASN1_TYPE_set_octetstring(ASN1_TYPE *a, unsigned char *data, int len);
|
||||||
|
int ASN1_TYPE_get_octetstring(const ASN1_TYPE *a, unsigned char *data, int max_len);
|
||||||
|
int ASN1_TYPE_set_int_octetstring(ASN1_TYPE *a, long num,
|
||||||
|
unsigned char *data, int len);
|
||||||
|
int ASN1_TYPE_get_int_octetstring(const ASN1_TYPE *a, long *num,
|
||||||
|
unsigned char *data, int max_len);
|
||||||
|
|
||||||
|
void *ASN1_item_unpack(const ASN1_STRING *oct, const ASN1_ITEM *it);
|
||||||
|
|
||||||
|
ASN1_STRING *ASN1_item_pack(void *obj, const ASN1_ITEM *it,
|
||||||
|
ASN1_OCTET_STRING **oct);
|
||||||
|
|
||||||
|
void ASN1_STRING_set_default_mask(unsigned long mask);
|
||||||
|
int ASN1_STRING_set_default_mask_asc(const char *p);
|
||||||
|
unsigned long ASN1_STRING_get_default_mask(void);
|
||||||
|
int ASN1_mbstring_copy(ASN1_STRING **out, const unsigned char *in, int len,
|
||||||
|
int inform, unsigned long mask);
|
||||||
|
int ASN1_mbstring_ncopy(ASN1_STRING **out, const unsigned char *in, int len,
|
||||||
|
int inform, unsigned long mask,
|
||||||
|
long minsize, long maxsize);
|
||||||
|
|
||||||
|
ASN1_STRING *ASN1_STRING_set_by_NID(ASN1_STRING **out,
|
||||||
|
const unsigned char *in, int inlen,
|
||||||
|
int inform, int nid);
|
||||||
|
ASN1_STRING_TABLE *ASN1_STRING_TABLE_get(int nid);
|
||||||
|
int ASN1_STRING_TABLE_add(int, long, long, unsigned long, unsigned long);
|
||||||
|
void ASN1_STRING_TABLE_cleanup(void);
|
||||||
|
|
||||||
|
/* ASN1 template functions */
|
||||||
|
|
||||||
|
/* Old API compatible functions */
|
||||||
|
ASN1_VALUE *ASN1_item_new(const ASN1_ITEM *it);
|
||||||
|
void ASN1_item_free(ASN1_VALUE *val, const ASN1_ITEM *it);
|
||||||
|
ASN1_VALUE *ASN1_item_d2i(ASN1_VALUE **val, const unsigned char **in,
|
||||||
|
long len, const ASN1_ITEM *it);
|
||||||
|
int ASN1_item_i2d(ASN1_VALUE *val, unsigned char **out, const ASN1_ITEM *it);
|
||||||
|
int ASN1_item_ndef_i2d(ASN1_VALUE *val, unsigned char **out,
|
||||||
|
const ASN1_ITEM *it);
|
||||||
|
|
||||||
|
void ASN1_add_oid_module(void);
|
||||||
|
void ASN1_add_stable_module(void);
|
||||||
|
|
||||||
|
ASN1_TYPE *ASN1_generate_nconf(const char *str, CONF *nconf);
|
||||||
|
ASN1_TYPE *ASN1_generate_v3(const char *str, X509V3_CTX *cnf);
|
||||||
|
int ASN1_str2mask(const char *str, unsigned long *pmask);
|
||||||
|
|
||||||
|
/* ASN1 Print flags */
|
||||||
|
|
||||||
|
/* Indicate missing OPTIONAL fields */
|
||||||
|
# define ASN1_PCTX_FLAGS_SHOW_ABSENT 0x001
|
||||||
|
/* Mark start and end of SEQUENCE */
|
||||||
|
# define ASN1_PCTX_FLAGS_SHOW_SEQUENCE 0x002
|
||||||
|
/* Mark start and end of SEQUENCE/SET OF */
|
||||||
|
# define ASN1_PCTX_FLAGS_SHOW_SSOF 0x004
|
||||||
|
/* Show the ASN1 type of primitives */
|
||||||
|
# define ASN1_PCTX_FLAGS_SHOW_TYPE 0x008
|
||||||
|
/* Don't show ASN1 type of ANY */
|
||||||
|
# define ASN1_PCTX_FLAGS_NO_ANY_TYPE 0x010
|
||||||
|
/* Don't show ASN1 type of MSTRINGs */
|
||||||
|
# define ASN1_PCTX_FLAGS_NO_MSTRING_TYPE 0x020
|
||||||
|
/* Don't show field names in SEQUENCE */
|
||||||
|
# define ASN1_PCTX_FLAGS_NO_FIELD_NAME 0x040
|
||||||
|
/* Show structure names of each SEQUENCE field */
|
||||||
|
# define ASN1_PCTX_FLAGS_SHOW_FIELD_STRUCT_NAME 0x080
|
||||||
|
/* Don't show structure name even at top level */
|
||||||
|
# define ASN1_PCTX_FLAGS_NO_STRUCT_NAME 0x100
|
||||||
|
|
||||||
|
int ASN1_item_print(BIO *out, ASN1_VALUE *ifld, int indent,
|
||||||
|
const ASN1_ITEM *it, const ASN1_PCTX *pctx);
|
||||||
|
ASN1_PCTX *ASN1_PCTX_new(void);
|
||||||
|
void ASN1_PCTX_free(ASN1_PCTX *p);
|
||||||
|
unsigned long ASN1_PCTX_get_flags(const ASN1_PCTX *p);
|
||||||
|
void ASN1_PCTX_set_flags(ASN1_PCTX *p, unsigned long flags);
|
||||||
|
unsigned long ASN1_PCTX_get_nm_flags(const ASN1_PCTX *p);
|
||||||
|
void ASN1_PCTX_set_nm_flags(ASN1_PCTX *p, unsigned long flags);
|
||||||
|
unsigned long ASN1_PCTX_get_cert_flags(const ASN1_PCTX *p);
|
||||||
|
void ASN1_PCTX_set_cert_flags(ASN1_PCTX *p, unsigned long flags);
|
||||||
|
unsigned long ASN1_PCTX_get_oid_flags(const ASN1_PCTX *p);
|
||||||
|
void ASN1_PCTX_set_oid_flags(ASN1_PCTX *p, unsigned long flags);
|
||||||
|
unsigned long ASN1_PCTX_get_str_flags(const ASN1_PCTX *p);
|
||||||
|
void ASN1_PCTX_set_str_flags(ASN1_PCTX *p, unsigned long flags);
|
||||||
|
|
||||||
|
ASN1_SCTX *ASN1_SCTX_new(int (*scan_cb) (ASN1_SCTX *ctx));
|
||||||
|
void ASN1_SCTX_free(ASN1_SCTX *p);
|
||||||
|
const ASN1_ITEM *ASN1_SCTX_get_item(ASN1_SCTX *p);
|
||||||
|
const ASN1_TEMPLATE *ASN1_SCTX_get_template(ASN1_SCTX *p);
|
||||||
|
unsigned long ASN1_SCTX_get_flags(ASN1_SCTX *p);
|
||||||
|
void ASN1_SCTX_set_app_data(ASN1_SCTX *p, void *data);
|
||||||
|
void *ASN1_SCTX_get_app_data(ASN1_SCTX *p);
|
||||||
|
|
||||||
|
const BIO_METHOD *BIO_f_asn1(void);
|
||||||
|
|
||||||
|
BIO *BIO_new_NDEF(BIO *out, ASN1_VALUE *val, const ASN1_ITEM *it);
|
||||||
|
|
||||||
|
int i2d_ASN1_bio_stream(BIO *out, ASN1_VALUE *val, BIO *in, int flags,
|
||||||
|
const ASN1_ITEM *it);
|
||||||
|
int PEM_write_bio_ASN1_stream(BIO *out, ASN1_VALUE *val, BIO *in, int flags,
|
||||||
|
const char *hdr, const ASN1_ITEM *it);
|
||||||
|
int SMIME_write_ASN1(BIO *bio, ASN1_VALUE *val, BIO *data, int flags,
|
||||||
|
int ctype_nid, int econt_nid,
|
||||||
|
STACK_OF(X509_ALGOR) *mdalgs, const ASN1_ITEM *it);
|
||||||
|
ASN1_VALUE *SMIME_read_ASN1(BIO *bio, BIO **bcont, const ASN1_ITEM *it);
|
||||||
|
int SMIME_crlf_copy(BIO *in, BIO *out, int flags);
|
||||||
|
int SMIME_text(BIO *in, BIO *out);
|
||||||
|
|
||||||
|
const ASN1_ITEM *ASN1_ITEM_lookup(const char *name);
|
||||||
|
const ASN1_ITEM *ASN1_ITEM_get(size_t i);
|
||||||
|
|
||||||
|
# ifdef __cplusplus
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user