Compare commits
687 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44871626cf | |||
| f661522b5d | |||
| a454b0ab78 | |||
| 75bf84d0d9 | |||
| c248994ed4 | |||
| a7a34ec629 | |||
| 8d469c5d0a | |||
| e6979d50b5 | |||
| 6e25b135a3 | |||
| 04e093a2c2 | |||
| 813f2edd95 | |||
| 337d753a33 | |||
| 1137c74f76 | |||
| 0c1451f757 | |||
| 5ab9ea48fd | |||
| ffcb623040 | |||
| bfe6929642 | |||
| 50d45011a8 | |||
| 2f7087d6d3 | |||
| 92b71588d0 | |||
| 4f3d018764 | |||
| a22e33eeb9 | |||
| 6a939e7495 | |||
| f8ba7cf190 | |||
| d1e135db4d | |||
| 61a17afe69 | |||
| 47fd691884 | |||
| 0d171c6b28 | |||
| f0c69d08b8 | |||
| 629bf5766d | |||
| 233bceeece | |||
| 6660ea7d91 | |||
| 4864b2ca45 | |||
| 92097b318d | |||
| 997898c99d | |||
| 1174e03885 | |||
| ff0f54d541 | |||
| 814964a100 | |||
| 7e154292a9 | |||
| 0f9cba1053 | |||
| a4e134589d | |||
| cd80a94f28 | |||
| 57c645a291 | |||
| 0cba200207 | |||
| 81582d7343 | |||
| 04e561fd54 | |||
| 5efbb5229d | |||
| 541e43eb18 | |||
| 7e679ff4c6 | |||
| 486b4b4c4c | |||
| 7d76bf7868 | |||
| db49077b9b | |||
| 16b845ab84 | |||
| 5c175fecf6 | |||
| 773976b265 | |||
| 80070bbdbe | |||
| 4c8d433b6c | |||
| 404f096d11 | |||
| d2ac927cec | |||
| 5e3d59d3d7 | |||
| 9cd2ce1309 | |||
| 9ed49730d4 | |||
| 39ebb48f58 | |||
| 1c29c70fba | |||
| 6993051529 | |||
| 4930087c4d | |||
| 795f0a013b | |||
| 213414778e | |||
| 7eac0ccaf8 | |||
| 6adc9dcb2d | |||
| be620908f9 | |||
| e4edfdb043 | |||
| 3b5028d1a4 | |||
| bc8c45bd59 | |||
| 63eb346a70 | |||
| 27ad691d23 | |||
| 747e920061 | |||
| 8d09f56a0e | |||
| 113a0e2c45 | |||
| 977215a098 | |||
| a7e65b47f9 | |||
| 7126055ad6 | |||
| 3de9765eaa | |||
| d4072eb295 | |||
| cac2bdbb81 | |||
| 66f0aee3f8 | |||
| b690dc5474 | |||
| c2fbe6ad91 | |||
| cf07c02398 | |||
| 42dc928ad5 | |||
| 11597f0aa7 | |||
| cdcd4d48f2 | |||
| a9af4e54a9 | |||
| 7eac609219 | |||
| fa761debc4 | |||
| 62e175f069 | |||
| d7d8c40565 | |||
| 64de13ab50 | |||
| 2f02939638 | |||
| 1d7c8697e9 | |||
| 7dea322bbd | |||
| 349ecb16ab | |||
| a3867735c1 | |||
| 5b087e9f70 | |||
| eed18223eb | |||
| 30d4d2a918 | |||
| 30f666c70e | |||
| 209fead0e8 | |||
| 5c6889bf6d | |||
| 7d24900756 | |||
| 79a75b9d19 | |||
| 29b64992bd | |||
| c9b14540f2 | |||
| 546843a26c | |||
| d03d260535 | |||
| 6946e3c7a2 | |||
| b79d328961 | |||
| c313797d93 | |||
| c8cb8e1346 | |||
| 6a9f8da14e | |||
| ff9260a0fd | |||
| 62bedb1609 | |||
| a519723d44 | |||
| 36191781ed | |||
| 61b6a49669 | |||
| e97845e46e | |||
| 6bba68207d | |||
| 0e17cccc06 | |||
| 918e922e40 | |||
| a08854ddfd | |||
| eb6f15c2b7 | |||
| 2cd9e31684 | |||
| 791d6624e2 | |||
| af41021271 | |||
| d726d939f4 | |||
| 748085e7bb | |||
| d57d19174b | |||
| efebe1828a | |||
| 06007e0597 | |||
| 3a868045d7 | |||
| e0a7ff1880 | |||
| 88d43bbd40 | |||
| 30ff319b13 | |||
| 9a0f48b799 | |||
| b52c8a1a8f | |||
| 3fde115670 | |||
| b6f4d8ff1e | |||
| a7d85a7dd5 | |||
| 9b238ab6c3 | |||
| f82ee97c05 | |||
| 35fb96f9f4 | |||
| 37371906d5 | |||
| 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 | |||
| 89ef16c02e | |||
| 58b6ed8d00 | |||
| 7d01e1a7a4 | |||
| ab769a1606 | |||
| 3ac9abbab1 | |||
| 288efd0726 | |||
| d2d0ed65d6 | |||
| e697ed72db | |||
| b657c746be | |||
| 947f8db2d5 | |||
| 15857efd36 | |||
| 3fd0f20e10 | |||
| a2e64fd7df | |||
| a620dc7d0c | |||
| 9d7a28e408 | |||
| 3244344fc7 | |||
| 75057f2d39 | |||
| bbec3402d9 | |||
| dcf4dac8dd | |||
| d98f484aaf | |||
| 0218a9ce14 | |||
| 0ec6dcd67e | |||
| 88f9b68db7 | |||
| 3c2fd32d1e | |||
| 6557cba307 | |||
| ae6f797436 | |||
| 3442a64f4d | |||
| 37ddccde0c | |||
| ffc59c6bd6 | |||
| 88f84a0c12 | |||
| 03ecf3e5ac | |||
| 617c8582b4 | |||
| ef3b28295b | |||
| 3bcd2ee068 | |||
| d4ff58b3ad | |||
| c797318ece | |||
| 82387d23f8 | |||
| 766e629be5 | |||
| b93aa42c0c | |||
| 36f132942f | |||
| e4c251e7ee | |||
| fb54bd5c78 | |||
| 8d4c86e113 | |||
| 7fafb8e0ff | |||
| fbcbe09255 | |||
| e336a4446a | |||
| ffb35b2cdd | |||
| 2d0af6281c | |||
| 472a7f6c8a | |||
| cd06559c66 | |||
| d833933aaa | |||
| dc3495d59b | |||
| e3a2e40043 | |||
| 31e1fb743e | |||
| bc59f11096 | |||
| 6d97775aa9 | |||
| 3fff34e08a | |||
| 15e856dccb | |||
| 07d04171c3 | |||
| 42bd93cb3a | |||
| 7d289f1134 | |||
| 214461e123 | |||
| b0144a3256 | |||
| 3171256c6e | |||
| 5c69f6716c | |||
| 6264781539 | |||
| 0225f534d0 | |||
| 284a31737e | |||
| b37a2dea57 | |||
| 5c865e7f36 | |||
| 04d9aea8c8 | |||
| b6f52db9c3 | |||
| 99d2e40683 | |||
| 02c4ed2724 | |||
| 5f4aab8f94 | |||
| ec65901003 | |||
| 915acee88d | |||
| 300d444f71 | |||
| f37ab40c2f | |||
| 16e285d926 | |||
| f2d122a275 | |||
| bfa5a6349e | |||
| a56689aea3 | |||
| 3a5ba820cb | |||
| ec69fef36f | |||
| ff38074f55 | |||
| 85d0ce0c40 | |||
| 777129ca90 | |||
| 06156c4d68 | |||
| 1c725b9dac | |||
| f761ee52db | |||
| 05e8cfcc0a | |||
| 912925ef2c | |||
| 4deb881ec8 | |||
| f55d6308ce | |||
| 44a3a141c0 | |||
| 37b5ba004c | |||
| b774b47213 | |||
| 74dc00445e | |||
| 3b4563d5ea | |||
| 38669817b4 | |||
| 8f1d3ae04e | |||
| 74ed95871b | |||
| cc5d67616c | |||
| eed7f09e6f | |||
| e3c1d23744 | |||
| c4b1200b43 | |||
| dff09f33a3 | |||
| 1f6b1dc2fe | |||
| 3f118dae93 | |||
| 91a30ff6fe | |||
| 5102669b06 | |||
| 2e2f09be00 | |||
| c402103fe3 | |||
| 5e5df8abc8 | |||
| d125eb7b16 | |||
| a116858493 | |||
| 5f3b333e98 | |||
| 80a37855c7 | |||
| 5db1ec8ec0 | |||
| 8911c58e50 | |||
| 780a64694d | |||
| 3c5ea9c8c3 | |||
| 40d1436ce3 | |||
| dbb02acd37 | |||
| 20c4eac4ef | |||
| b9f1142af7 | |||
| 38a6a2b74a | |||
| fd2421618a | |||
| 79a9ea7179 | |||
| 34a11c9262 | |||
| 84a9845c1d | |||
| 5b05220008 | |||
| b2bd7257e1 | |||
| 46a998c113 | |||
| 60cd951774 | |||
| d4f8d8f689 | |||
| 608a0ebb5b | |||
| f01a15d182 | |||
| 0268b4f958 | |||
| d71cf0eb98 | |||
| 10ab40f823 | |||
| 427edfa021 | |||
| 6f18831d5c | |||
| a3db09f422 | |||
| d185a05b1d | |||
| 78e575504a | |||
| 0a0be19b69 | |||
| 0792157e9d | |||
| cdd0ecf0b7 | |||
| 1ac721a35b | |||
| e49b1c92a2 | |||
| db4295bf83 | |||
| 824c37f9d5 | |||
| acf4426952 | |||
| e8c50342ab | |||
| 598995de3b | |||
| 01cf0cc649 | |||
| fa560f462f | |||
| f6e40118a9 | |||
| fe7148dbd4 | |||
| 60de065836 | |||
| 6f82f82abb | |||
| 42f18cb4ac | |||
| 1bbd0054c2 | |||
| acdde37a3a | |||
| ad40e12167 | |||
| 1b3322b5ee | |||
| 6340ec6c6d | |||
| babd92c8c0 | |||
| 7f1fe5f520 | |||
| 01458770d2 | |||
| 8d05f044f5 | |||
| f5680b59a5 | |||
| 0ecf86c7ed | |||
| 6789e8d497 | |||
| 7d0160d556 | |||
| f6a0990432 | |||
| 5d6094df97 | |||
| d98d4aeda2 | |||
| 852dcf5a2d | |||
| 82e5aa122d | |||
| fe237d1da3 | |||
| e199fcd2d9 | |||
| d7c6f63592 | |||
| 4b9c6b149a | |||
| d1e41e41a1 | |||
| 96dfe25a14 | |||
| f76d78607a | |||
| a96f688bb2 | |||
| 90a1e68c68 | |||
| b287606106 | |||
| a413185085 | |||
| aa1b283570 | |||
| f07c886711 | |||
| e66b1ebec9 | |||
| d06912e81a | |||
| 08bcd97594 | |||
| 49e51f5f6f | |||
| 4223a7fd30 | |||
| 6edd0ab540 | |||
| ce7146175a | |||
| 3176a85f35 | |||
| ad1c11bba5 | |||
| ac640a6842 | |||
| 8962497a8c | |||
| 83141d3f91 | |||
| 55f2e89bbe | |||
| 3558655b72 | |||
| 44cbf8adc1 | |||
| 686490ba70 | |||
| d0ecde1e16 | |||
| 9417908848 | |||
| 93b0073467 | |||
| 1434be262c | |||
| 75aabd6471 | |||
| bafa2addd3 | |||
| 32b787e77c | |||
| 43b58b7a5e | |||
| 9ae1fe2696 | |||
| 6d0f34e2c4 | |||
| f7d91b5107 | |||
| a3c95480d8 | |||
| 732311c2a4 | |||
| 043c9a978e | |||
| 36b248be4b | |||
| 8e247ad9a6 | |||
| a2de98c91a | |||
| 81d1e615bf | |||
| 244fae07ab | |||
| e7d96f0ac2 | |||
| 4555b3c74c | |||
| 8c13186757 | |||
| feafc4ef3c | |||
| 5c03295478 | |||
| dc3a923041 | |||
| eccba807bc | |||
| 35fa8f5bcc | |||
| 0380910588 | |||
| e85bb4372e | |||
| 2c345cd6c2 | |||
| b5c96cbb53 | |||
| b21ee5ca31 | |||
| 9c7bff6c75 | |||
| 3d470d9aed | |||
| b2a36c2c73 | |||
| 7978687bfc | |||
| f612ec80e2 | |||
| 7df1a39fcb | |||
| a539ac62ec | |||
| fa52e5edc2 | |||
| 3ca681f050 | |||
| 8086c3d46b | |||
| 928fca843f | |||
| 25d74785d0 | |||
| e12a8e7946 | |||
| b14f2ce219 | |||
| d31be3d64e | |||
| 0704f2aaf6 | |||
| 832e52ac74 | |||
| f5444551b2 | |||
| 3143797b55 | |||
| cc9b1aeaab | |||
| 3d177e97e4 | |||
| 6c3aaedc83 | |||
| bf84ebef6d | |||
| 8991b29329 | |||
| fa84575be5 | |||
| 0432d5725b | |||
| 8e7b144339 | |||
| fc629db653 | |||
| d5863e1bef | |||
| c2c3a6b37c | |||
| e701699dea | |||
| 17179bd027 | |||
| b2f210700d | |||
| f0e85c4c53 | |||
| 92f8425ace | |||
| 6ad001e8be | |||
| b6e4d5528b | |||
| 0f0b83badc | |||
| 453fbb5f58 | |||
| e7dc3a4c11 | |||
| d68b2382cf | |||
| 1b5330323c | |||
| 8aba4888e1 | |||
| 1c3b9a3859 | |||
| e8f04f5a3b | |||
| 56b814e877 | |||
| 628ccd39d6 | |||
| 59db3f9b62 | |||
| 416f922b56 | |||
| b52a86e6cc | |||
| e523b5069e | |||
| e8ae8d9807 | |||
| 64e56a861d | |||
| c1bcd09c9b | |||
| 574258804f | |||
| 21ea3d8a2b | |||
| 6de4288a85 | |||
| a107b5e652 | |||
| b02db2c182 | |||
| f8a04cda7a | |||
| 226e8edefc | |||
| 9b90b30a1f | |||
| 2ed245b25a | |||
| 4b769839d0 | |||
| 239dd1d5a1 | |||
| 37509cce9b | |||
| 227c71549b | |||
| a10d8334f3 | |||
| f88c9904fb | |||
| 0fc61e52dd | |||
| 5e44c33bb6 | |||
| df3655e958 | |||
| fe43e13145 | |||
| acd3aad8d9 | |||
| 811b4b4f22 | |||
| 7db3b9f401 | |||
| a5a099cf43 | |||
| ba605643bb | |||
| 96e98c1abb | |||
| 5de6f6ae2b | |||
| 0685722773 | |||
| 29df3b2859 | |||
| fc6f859ced | |||
| 6b21a5416f | |||
| 74e7c8bbf1 | |||
| 757075b16a | |||
| e8903c4d48 | |||
| 98262d16ee | |||
| 339506cf10 | |||
| 63bd5df09b | |||
| 32af2d0831 | |||
| 242b03d4b5 | |||
| 87a62666ac | |||
| 2dcf5486da | |||
| 60d3d8b3ae | |||
| e9141d65fe | |||
| aae591daec | |||
| a5ca8a7472 | |||
| 36f8cc02cb | |||
| 55b9645651 | |||
| d30ecbed5b | |||
| 0bbd27f04c | |||
| 3c53fb7403 | |||
| 7a81950819 | |||
| 74f212c702 | |||
| 36be943854 | |||
| 26a4fc75a5 | |||
| a5ec5fc265 | |||
| 541ac44be4 | |||
| 117b555fcd | |||
| a10cd04441 | |||
| 53dccbde2a | |||
| 56625dfe4b | |||
| 2eab5a3b7b | |||
| f9e811862a | |||
| 25ccc3d0e1 | |||
| 8853bf0670 | |||
| 71fa3a824b | |||
| 56fd50834c | |||
| 48ba812cf6 | |||
| 019dc6d45f | |||
| cbcb784a79 | |||
| 39fa0258ad | |||
| d0dd5bfa8c | |||
| b948c47618 | |||
| 18cae8ac53 | |||
| 0576231dfc | |||
| 6ad35a83dd | |||
| 33d4dfc745 | |||
| f3bf63a668 | |||
| 2dbb7395a4 | |||
| 7c1eb80d62 | |||
| f2bf093691 | |||
| 2f002bfa4a | |||
| 4a19038d54 | |||
| 15fb3dd92c | |||
| e0982d3961 | |||
| 7fb2f15f54 | |||
| f93dbb4116 | |||
| bc34fe3a9f | |||
| bbe49491c1 | |||
| d5ccb80f26 | |||
| 50fd15379a | |||
| ed479f1155 | |||
| 04db9ba714 | |||
| 6a973e3248 | |||
| 96d9e4977b | |||
| 5a3897f22a | |||
| ceef00b79a | |||
| 94ee24ea11 | |||
| 1a201f2e94 | |||
| e0c6d41d4b | |||
| 44a0ae86d2 | |||
| 06822ad385 | |||
| 3be52280ba | |||
| 5142f978cf | |||
| 667ffd4dfd | |||
| 17626f1853 | |||
| 5c79567a2c | |||
| 0f5fd9af62 | |||
| 99643537d1 | |||
| 47650386e0 | |||
| aa3fc34646 | |||
| 92f5f1ac71 | |||
| eb739f73c7 | |||
| 20a646106b | |||
| 0dc14517cd | |||
| 04713c007b | |||
| 1cac7660b8 | |||
| edb286f9af | |||
| fb15ff99ca | |||
| a455e75e37 | |||
| 2b452e51f9 | |||
| 9d2b6f8854 | |||
| 3be10a1b59 | |||
| 01950c25a8 | |||
| 7ad1ebd0e8 | |||
| ee01a8b5a0 | |||
| 23c54f6813 | |||
| ceef4510fb | |||
| 042a6b943e | |||
| e114b73654 | |||
| da0a505978 | |||
| cb6d4a385c | |||
| 2806aee0fc | |||
| 52736f5162 | |||
| 6d45ad7fe8 | |||
| 2fc53644bc | |||
| b33eaec493 | |||
| 63d6f3ac78 | |||
| fd4caac013 | |||
| ada875cdb0 | |||
| 49ddfa573d | |||
| b58ac367ee | |||
| cf62b4ed95 | |||
| b05c62e141 | |||
| 095556106c | |||
| 5cdd72a45c | |||
| 5d84f8af43 | |||
| d9483d9214 | |||
| 250475830f | |||
| b8a0a823e0 | |||
| 6a54d669a3 | |||
| 62559c4e66 | |||
| e04ecaaf7a | |||
| fa4706c95f | |||
| 7067c0e02e | |||
| cc71ce6180 | |||
| f409a3583c | |||
| ac7504e017 | |||
| 345bd3f7c1 | |||
| 2e2960ec69 | |||
| e93b103d1e | |||
| 22977a4c5b | |||
| 7da5d5322b | |||
| 49e2c40ba4 | |||
| 8041a004c2 | |||
| db62d78e04 | |||
| bd79318b1e | |||
| 2736bd9165 | |||
| b6bd48584f | |||
| 7b4f3c975a | |||
| b165fadc55 | |||
| 274e0d0557 | |||
| 7594e51a18 | |||
| bf22819b53 | |||
| 3dea4b15e0 | |||
| 5836b3292b | |||
| a8fd49a234 | |||
| 006ad72eb2 | |||
| dc254e1ee5 | |||
| b0d31a4d35 | |||
| 24155feea4 | |||
| db0a4e35c6 | |||
| 68ef98d346 | |||
| f23bb9fac1 | |||
| d20dde0b6d | |||
| f76b30d109 | |||
| ee1a047cde | |||
| 4c533fedfd | |||
| f8ab7b8e13 | |||
| 46c5eaf0e1 | |||
| 1d6b5a35bd | |||
| 1ff6ee14ac | |||
| d2e51e97c0 | |||
| 9f94465979 | |||
| d83526ff5c | |||
| 1d6b7e1b2e | |||
| 1c9458d056 | |||
| 4e29f2ae8b | |||
| 69321636b5 | |||
| d190b254bd | |||
| 005a96f3d3 | |||
| e39e0910a1 | |||
| 56a6cee8f2 |
@@ -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
|
||||||
+7
-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,6 +33,8 @@ 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/
|
||||||
|
*.iml
|
||||||
|
|
||||||
# Compiled JNI libraries folder
|
# Compiled JNI libraries folder
|
||||||
**/jniLibs
|
**/jniLibs
|
||||||
|
app/.externalNativeBuild/
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "moonlight-common"]
|
||||||
|
path = moonlight-common
|
||||||
|
url = https://github.com/moonlight-stream/moonlight-common.git
|
||||||
@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
|
|||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
local data_start = -1
|
local data_start = -1
|
||||||
while i < buf:len do
|
while i < buf:len() do
|
||||||
-- Make sure we have a potential start sequence and type
|
-- Make sure we have a potential start sequence and type
|
||||||
if buf:len() - i < 5 then
|
if buf:len() - i < 5 then
|
||||||
-- We need more data
|
-- We need more data
|
||||||
|
|||||||
@@ -1,44 +1,42 @@
|
|||||||
#Moonlight
|
# Moonlight Android
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[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-android/wiki) for more detailed information or a troubleshooting guide.
|
## Features
|
||||||
|
|
||||||
##Features
|
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
##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), [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
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
|
|
||||||
##Usage
|
## Usage
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC and add the PIN if needed
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
##Contribute
|
## Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -46,7 +44,13 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
##Authors
|
## Building
|
||||||
|
* Install Android Studio and the Android NDK
|
||||||
|
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
||||||
|
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||||
|
* Build the APK using Android Studio
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||||
|
|||||||
-120
@@ -1,120 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
|
||||||
<component name="FacetManager">
|
|
||||||
<facet type="android-gradle" name="Android-Gradle">
|
|
||||||
<configuration>
|
|
||||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
|
||||||
</configuration>
|
|
||||||
</facet>
|
|
||||||
<facet type="android" name="Android">
|
|
||||||
<configuration>
|
|
||||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
|
||||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
|
||||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
|
||||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
|
||||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
|
||||||
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
|
|
||||||
<afterSyncTasks>
|
|
||||||
<task>generateNonRootDebugAndroidTestSources</task>
|
|
||||||
<task>generateNonRootDebugSources</task>
|
|
||||||
</afterSyncTasks>
|
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
|
||||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
|
||||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
|
||||||
</configuration>
|
|
||||||
</facet>
|
|
||||||
</component>
|
|
||||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
|
||||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/nonRoot/debug" isTestSource="false" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/aidl" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/aidl" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/java" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/jni" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/rs" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/res" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/resources" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/assets" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/aidl" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/java" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/jni" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/rs" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.9-patched" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
+91
-44
@@ -1,75 +1,122 @@
|
|||||||
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 23
|
buildToolsVersion '28.0.3'
|
||||||
buildToolsVersion "23.0.2"
|
compileSdkVersion 28
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 28
|
||||||
|
|
||||||
versionName "4.0.2"
|
versionName "7.2"
|
||||||
versionCode = 79
|
versionCode = 191
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "root"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
root {
|
||||||
|
// Android O has native mouse capture, so don't show the rooted
|
||||||
|
// version to devices running O on the Play Store.
|
||||||
|
maxSdkVersion 25
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
arguments "PRODUCT_FLAVOR=root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
arguments "PRODUCT_FLAVOR=nonRoot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
|
dimension "root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
// To whomever is releasing/using an APK in release mode with
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
// Moonlight's official application ID, please stop. I see every
|
||||||
|
// single one of your crashes in my Play Console and it makes
|
||||||
|
// Moonlight's reliability look worse and makes it more difficult
|
||||||
|
// to distinguish real crashes from your crashy VR app. Seriously,
|
||||||
|
// 44 of the *same* native crash in 72 hours and a few each of
|
||||||
|
// several other crashes.
|
||||||
|
//
|
||||||
|
// This is technically not your fault. I would have hoped Google
|
||||||
|
// would validate the signature of the APK before attributing
|
||||||
|
// the crash to it. I asked their Play Store support about this
|
||||||
|
// and they said they don't and don't have plans to, so that sucks.
|
||||||
|
//
|
||||||
|
// In any case, it's bad form to release an APK using someone
|
||||||
|
// else's application ID. There is no legitimate reason, that
|
||||||
|
// anyone would need to comment out the following line, except me
|
||||||
|
// when I release an official signed Moonlight build. If you feel
|
||||||
|
// like doing so would solve something, I can tell you it will not.
|
||||||
|
// You can't upgrade an app while retaining data without having the
|
||||||
|
// same signature as the official version. Nor can you post it on
|
||||||
|
// the Play Store, since that application ID is already taken.
|
||||||
|
// Reputable APK hosting websites similarly validate the signature
|
||||||
|
// is consistent with the Play Store and won't allow an APK that
|
||||||
|
// isn't signed the same as the original.
|
||||||
|
//
|
||||||
|
// I wish any and all people using Moonlight as the basis of other
|
||||||
|
// cool projects the best of luck with their efforts. All I ask
|
||||||
|
// is to please change the applicationId before you publish.
|
||||||
|
//
|
||||||
|
// TL;DR: Leave the following line alone!
|
||||||
|
applicationIdSuffix ".unofficial"
|
||||||
|
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.main.jni.srcDirs = []
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
path "src/main/jni/Android.mk"
|
||||||
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
|
|
||||||
Properties properties = new Properties()
|
|
||||||
properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
|
||||||
def ndkDir = properties.getProperty('ndk.dir')
|
|
||||||
|
|
||||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
|
||||||
commandLine "$ndkDir\\ndk-build.cmd",
|
|
||||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
|
||||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
|
||||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
|
||||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
commandLine "$ndkDir/ndk-build",
|
|
||||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
|
||||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
|
||||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
|
||||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(JavaCompile) {
|
|
||||||
compileTask -> compileTask.dependsOn ndkBuild
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||||
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
|
|
||||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
|
implementation project(':moonlight-common')
|
||||||
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
|
|
||||||
|
|
||||||
compile files('libs/jmdns-3.4.2.jar')
|
|
||||||
compile files('libs/limelight-common.jar')
|
|
||||||
compile files('libs/tinyrtsp.jar')
|
|
||||||
compile files('libs/jcodec-0.1.9-patched.jar')
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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,78 +4,119 @@
|
|||||||
|
|
||||||
<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-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature
|
||||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
android:name="android.hardware.touchscreen"
|
||||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
<uses-feature
|
||||||
|
android:name="android.hardware.wifi"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.gamepad"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.usb.host"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- Disable legacy input emulation on ChromeOS -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.type.pc"
|
||||||
|
android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:theme="@style/AppTheme" >
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:isGame="true"
|
||||||
|
android:banner="@drawable/atv_banner"
|
||||||
|
android:appCategory="game"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<!-- Samsung multi-window support -->
|
<!-- Samsung multi-window support -->
|
||||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
<uses-library
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
android:name="com.sec.android.app.multiwindow"
|
||||||
|
android:required="false" />
|
||||||
<!-- Launcher for traditional devices -->
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.sec.android.support.multiwindow"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
|
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
|
||||||
|
in each activity even though it is implied by targeting API 24+ -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
android:name=".PcView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
android:resizeableActivity="true"
|
||||||
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||||
<!-- Launcher for Android TV devices -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcViewTv"
|
android:name=".ShortcutTrampoline"
|
||||||
android:logo="@drawable/atv_banner"
|
android:noHistory="true"
|
||||||
android:icon="@drawable/atv_banner"
|
android:exported="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">
|
||||||
<intent-filter>
|
<meta-data
|
||||||
<action android:name="android.intent.action.MAIN" />
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
android:value="com.limelight.PcView" />
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppView"
|
android:name=".AppView"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
android:resizeableActivity="true"
|
||||||
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.StreamSettings"
|
android:name=".preferences.StreamSettings"
|
||||||
android:label="Streaming Settings" >
|
android:resizeableActivity="true"
|
||||||
|
android:label="Streaming Settings">
|
||||||
<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>
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.AddComputerManually"
|
android:name=".preferences.AddComputerManually"
|
||||||
android:label="Add Computer Manually" >
|
android:resizeableActivity="true"
|
||||||
|
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:screenOrientation="sensorLandscape"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||||
android:theme="@style/StreamTheme"
|
android:screenOrientation="userLandscape"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
android:noHistory="true"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:theme="@style/StreamTheme">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.AppView" />
|
android:value="com.limelight.AppView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".discovery.DiscoveryService"
|
android:name=".discovery.DiscoveryService"
|
||||||
android:label="mDNS PC Auto-Discovery Service" />
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
@@ -85,6 +126,15 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".binding.input.driver.UsbDriverService"
|
android:name=".binding.input.driver.UsbDriverService"
|
||||||
android:label="Usb Driver Service" />
|
android:label="Usb Driver Service" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".HelpActivity"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="com.limelight.PcView" />
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -1,9 +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.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -11,12 +10,14 @@ import com.limelight.grid.AppGridAdapter;
|
|||||||
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.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
@@ -25,7 +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.content.res.Configuration;
|
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,13 +39,17 @@ 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;
|
||||||
|
private ShortcutHelper shortcutHelper;
|
||||||
|
|
||||||
private ComputerDetails computer;
|
private ComputerDetails computer;
|
||||||
private ComputerManagerService.ApplistPoller poller;
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
@@ -50,11 +57,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
private String lastRawApplist;
|
private String lastRawApplist;
|
||||||
private int lastRunningAppId;
|
private int lastRunningAppId;
|
||||||
private boolean suspendGridUpdates;
|
private boolean suspendGridUpdates;
|
||||||
|
private boolean inForeground;
|
||||||
|
|
||||||
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";
|
||||||
@@ -72,32 +82,57 @@ 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) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
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).
|
||||||
|
// This must be done _before_ startComputerUpdates()
|
||||||
|
// so the initial serverinfo response can update the running
|
||||||
|
// icon.
|
||||||
|
populateAppGridWithCache();
|
||||||
|
|
||||||
// Start updates
|
// Start updates
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
|
|
||||||
// Load the app grid with cached data (if possible)
|
runOnUiThread(new Runnable() {
|
||||||
populateAppGridWithCache();
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isFinishing() || isChangingConfigurations()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
// Despite my best efforts to catch all conditions that could
|
||||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
// cause the activity to be destroyed when we try to commit
|
||||||
.commitAllowingStateLoss();
|
// I haven't been able to, so we have this try-catch block.
|
||||||
|
try {
|
||||||
|
getFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
@@ -108,20 +143,21 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder == null) {
|
// Don't start polling if we're not bound or in the foreground
|
||||||
|
if (managerBinder == null || !inForeground) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerUpdated(ComputerDetails details) {
|
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||||
// Do nothing if updates are suspended
|
// Do nothing if updates are suspended
|
||||||
if (suspendGridUpdates) {
|
if (suspendGridUpdates) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +175,24 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close immediately if the PC is no longer paired
|
||||||
|
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Disable shortcuts referencing this PC for now
|
||||||
|
shortcutHelper.disableShortcut(details.uuid,
|
||||||
|
getResources().getString(R.string.scut_not_paired));
|
||||||
|
|
||||||
|
// Display a toast to the user and quit the activity
|
||||||
|
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// App list is the same or empty
|
// App list is the same or empty
|
||||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
|
||||||
@@ -157,12 +211,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
if (blockingLoadSpinner != null) {
|
||||||
blockingLoadSpinner.dismiss();
|
blockingLoadSpinner.dismiss();
|
||||||
blockingLoadSpinner = null;
|
blockingLoadSpinner = null;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,12 +247,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
// Assume we're in the foreground when created to avoid a race
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
// between binding to CMS and onResume()
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
inForeground = true;
|
||||||
config.locale = new Locale(locale);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
}
|
|
||||||
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
setContentView(R.layout.activity_app_view);
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
@@ -203,10 +261,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
TextView label = (TextView) findViewById(R.id.appListText);
|
|
||||||
setTitle(labelText);
|
TextView label = findViewById(R.id.appListText);
|
||||||
label.setText(labelText);
|
setTitle(computerName);
|
||||||
|
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,
|
||||||
@@ -220,7 +283,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();
|
||||||
@@ -252,6 +315,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
// Display a decoder crash notification if we've returned after a crash
|
||||||
|
UiHelper.showDecoderCrashDialog(this);
|
||||||
|
|
||||||
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,38 +326,41 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
|
inForeground = false;
|
||||||
stopComputerUpdates();
|
stopComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRunningAppId() {
|
|
||||||
int runningAppId = -1;
|
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
if (app.app.getIsRunning()) {
|
|
||||||
runningAppId = app.app.getAppId();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runningAppId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
int runningAppId = getRunningAppId();
|
if (lastRunningAppId != 0) {
|
||||||
if (runningAppId != -1) {
|
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
||||||
if (runningAppId == selectedApp.app.getAppId()) {
|
|
||||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
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
|
||||||
@@ -302,7 +372,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
|
||||||
@@ -323,8 +393,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() {
|
||||||
@@ -342,6 +411,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(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -358,19 +440,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
// There can only be one or zero apps running.
|
// There can only be one or zero apps running.
|
||||||
if (existingApp.app.getIsRunning() &&
|
if (existingApp.isRunning &&
|
||||||
existingApp.app.getAppId() == details.runningGameId) {
|
existingApp.app.getAppId() == details.runningGameId) {
|
||||||
// This app was running and still is, so we're done now
|
// This app was running and still is, so we're done now
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (existingApp.app.getAppId() == details.runningGameId) {
|
else if (existingApp.app.getAppId() == details.runningGameId) {
|
||||||
// This app wasn't running but now is
|
// This app wasn't running but now is
|
||||||
existingApp.app.setIsRunning(true);
|
existingApp.isRunning = true;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
else if (existingApp.app.getIsRunning()) {
|
else if (existingApp.isRunning) {
|
||||||
// This app was running but now isn't
|
// This app was running but now isn't
|
||||||
existingApp.app.setIsRunning(false);
|
existingApp.isRunning = false;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -400,10 +482,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
// Found the app; update its properties
|
// Found the app; update its properties
|
||||||
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
|
||||||
existingApp.app.setIsRunning(app.getIsRunning());
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
existingApp.app.setAppName(app.getAppName());
|
existingApp.app.setAppName(app.getAppName());
|
||||||
updated = true;
|
updated = true;
|
||||||
@@ -473,7 +551,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
|
||||||
// Only open the context menu if something is running, otherwise start it
|
// Only open the context menu if something is running, otherwise start it
|
||||||
if (getRunningAppId() != -1) {
|
if (lastRunningAppId != 0) {
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else {
|
} else {
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
@@ -486,6 +564,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
public class AppObject {
|
public class AppObject {
|
||||||
public final NvApp app;
|
public final NvApp app;
|
||||||
|
public boolean isRunning;
|
||||||
|
|
||||||
public AppObject(NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
if (app == null) {
|
if (app == null) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
|
||||||
|
public class HelpActivity extends Activity {
|
||||||
|
|
||||||
|
private SpinnerDialog loadingDialog;
|
||||||
|
private WebView webView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
webView = new WebView(this);
|
||||||
|
setContentView(webView);
|
||||||
|
|
||||||
|
// These allow the user to zoom the page
|
||||||
|
webView.getSettings().setBuiltInZoomControls(true);
|
||||||
|
webView.getSettings().setDisplayZoomControls(false);
|
||||||
|
|
||||||
|
// This sets the view to display the whole page by default
|
||||||
|
webView.getSettings().setUseWideViewPort(true);
|
||||||
|
webView.getSettings().setLoadWithOverviewMode(true);
|
||||||
|
|
||||||
|
// This allows the links to places on the same page to work
|
||||||
|
webView.getSettings().setJavaScriptEnabled(true);
|
||||||
|
|
||||||
|
webView.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||||
|
if (loadingDialog == null) {
|
||||||
|
loadingDialog = SpinnerDialog.displayDialog(HelpActivity.this,
|
||||||
|
getResources().getString(R.string.help_loading_title),
|
||||||
|
getResources().getString(R.string.help_loading_msg), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
if (loadingDialog != null) {
|
||||||
|
loadingDialog.dismiss();
|
||||||
|
loadingDialog = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
|
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
||||||
|
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
webView.loadUrl(getIntent().getData().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// Back goes back through the WebView history
|
||||||
|
// until no more history remains
|
||||||
|
if (webView.canGoBack()) {
|
||||||
|
webView.goBack();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ package com.limelight;
|
|||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
@@ -18,20 +16,26 @@ import com.limelight.nvstream.http.PairingManager;
|
|||||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||||
import com.limelight.preferences.AddComputerManually;
|
import com.limelight.preferences.AddComputerManually;
|
||||||
|
import com.limelight.preferences.GlPreferences;
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.preferences.StreamSettings;
|
import com.limelight.preferences.StreamSettings;
|
||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
|
import com.limelight.utils.HelpLauncher;
|
||||||
import com.limelight.utils.ServerHelper;
|
import com.limelight.utils.ServerHelper;
|
||||||
|
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;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.opengl.GLSurfaceView;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
@@ -49,11 +53,17 @@ 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.opengles.GL10;
|
||||||
|
|
||||||
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private RelativeLayout noPcFoundLayout;
|
private RelativeLayout noPcFoundLayout;
|
||||||
private PcGridAdapter pcGridAdapter;
|
private PcGridAdapter pcGridAdapter;
|
||||||
|
private ShortcutHelper shortcutHelper;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private boolean freezeUpdates, runningPolling, hasResumed;
|
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
@@ -83,11 +93,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
// Reinitialize views just in case orientation changed
|
// Only reinitialize views if completeOnCreate() was called
|
||||||
initializeViews();
|
// before this callback. If it was not, completeOnCreate() will
|
||||||
|
// handle initializing views with the config change accounted for.
|
||||||
|
// This is not prone to races because both callbacks are invoked
|
||||||
|
// in the main thread.
|
||||||
|
if (completeOnCreateCalled) {
|
||||||
|
// Reinitialize views just in case orientation changed
|
||||||
|
initializeViews();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static int APP_LIST_ID = 1;
|
private final static int APP_LIST_ID = 1;
|
||||||
@@ -97,6 +115,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);
|
||||||
@@ -107,8 +126,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
|
|
||||||
// Setup the list view
|
// Setup the list view
|
||||||
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
||||||
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
||||||
|
ImageButton helpButton = findViewById(R.id.helpButton);
|
||||||
|
|
||||||
settingsButton.setOnClickListener(new OnClickListener() {
|
settingsButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -123,12 +143,18 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
helpButton.setOnClickListener(new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
HelpLauncher.launchSetupGuide(PcView.this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
getFragmentManager().beginTransaction()
|
getFragmentManager().beginTransaction()
|
||||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
|
|
||||||
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
|
||||||
if (pcGridAdapter.getCount() == 0) {
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
@@ -142,12 +168,55 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
// Assume we're in the foreground when created to avoid a race
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
// between binding to CMS and onResume()
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
inForeground = true;
|
||||||
config.locale = new Locale(locale);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
// Create a GLSurfaceView to fetch GLRenderer unless we have
|
||||||
|
// a cached result already.
|
||||||
|
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
||||||
|
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
|
||||||
|
GLSurfaceView surfaceView = new GLSurfaceView(this);
|
||||||
|
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
|
||||||
|
@Override
|
||||||
|
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
|
||||||
|
// Save the GLRenderer string so we don't need to do this next time
|
||||||
|
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
|
||||||
|
glPrefs.savedFingerprint = Build.FINGERPRINT;
|
||||||
|
glPrefs.writePreferences();
|
||||||
|
|
||||||
|
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
completeOnCreate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrawFrame(GL10 gl10) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setContentView(surfaceView);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
|
||||||
|
completeOnCreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeOnCreate() {
|
||||||
|
completeOnCreateCalled = true;
|
||||||
|
|
||||||
|
shortcutHelper = new ShortcutHelper(this);
|
||||||
|
|
||||||
|
UiHelper.setLocale(this);
|
||||||
|
|
||||||
// Bind to the computer manager service
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||||
@@ -161,11 +230,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
if (managerBinder != null) {
|
// Only allow polling to start if we're bound to CMS, polling is not already running,
|
||||||
if (runningPolling) {
|
// and our activity is in the foreground.
|
||||||
return;
|
if (managerBinder != null && !runningPolling && inForeground) {
|
||||||
}
|
|
||||||
|
|
||||||
freezeUpdates = false;
|
freezeUpdates = false;
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -215,7 +282,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
hasResumed = true;
|
// Display a decoder crash notification if we've returned after a crash
|
||||||
|
UiHelper.showDecoderCrashDialog(this);
|
||||||
|
|
||||||
|
inForeground = true;
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +293,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
hasResumed = false;
|
inForeground = false;
|
||||||
stopComputerUpdates(false);
|
stopComputerUpdates(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,8 +315,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));
|
||||||
}
|
}
|
||||||
@@ -266,19 +336,20 @@ 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
|
||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
||||||
// We don't want to start computer updates again, so we need to keep track of whether we're paused.
|
// startComputerUpdates() manages this and won't actual start polling until the activity
|
||||||
if (hasResumed) {
|
// returns to the foreground.
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doPair(final ComputerDetails computer) {
|
private void doPair(final ComputerDetails computer) {
|
||||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
if (computer.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;
|
||||||
}
|
}
|
||||||
@@ -302,23 +373,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Stop updates and wait while pairing
|
// Stop updates and wait while pairing
|
||||||
stopComputerUpdates(true);
|
stopComputerUpdates(true);
|
||||||
|
|
||||||
InetAddress addr = null;
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
|
||||||
addr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.warning("Unknown reachability - using local IP");
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
|
||||||
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;
|
||||||
@@ -330,17 +389,29 @@ 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(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.PAIRED) {
|
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||||
|
message = getResources().getString(R.string.pair_already_in_progress);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
// a refresh before reading pair state again
|
||||||
|
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should be no other values
|
// Should be no other values
|
||||||
@@ -351,7 +422,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();
|
||||||
}
|
}
|
||||||
@@ -368,14 +439,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toastSuccess) {
|
if (toastSuccess) {
|
||||||
// Open the app list after a successful pairing attemp
|
// Open the app list after a successful pairing attempt
|
||||||
doAppList(computer);
|
doAppList(computer);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Start polling again if we're still in the foreground
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start polling again
|
|
||||||
startComputerUpdates();
|
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -391,7 +463,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() {
|
||||||
@@ -415,7 +486,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;
|
||||||
}
|
}
|
||||||
@@ -431,21 +503,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
NvHTTP httpConn;
|
NvHTTP httpConn;
|
||||||
String message;
|
String message;
|
||||||
try {
|
try {
|
||||||
InetAddress addr = null;
|
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
|
||||||
addr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.warning("Unknown reachability - using local IP");
|
|
||||||
addr = computer.localIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
|
||||||
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();
|
||||||
@@ -463,8 +523,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;
|
||||||
@@ -479,7 +540,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doAppList(ComputerDetails computer) {
|
private void doAppList(ComputerDetails computer) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -490,7 +551,7 @@ 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);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,12 +573,21 @@ 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;
|
||||||
|
}
|
||||||
|
managerBinder.removeComputer(computer.details.name);
|
||||||
|
removeComputer(computer.details);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case APP_LIST_ID:
|
case APP_LIST_ID:
|
||||||
@@ -530,7 +600,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
@@ -543,13 +613,16 @@ 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), 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);
|
||||||
}
|
}
|
||||||
@@ -560,6 +633,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
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
|
||||||
|
shortcutHelper.disableShortcut(details.uuid,
|
||||||
|
getResources().getString(R.string.scut_deleted_pc));
|
||||||
|
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
pcGridAdapter.notifyDataSetChanged();
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
@@ -586,6 +663,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a launcher shortcut for this PC
|
||||||
|
if (details.pairState == PairState.PAIRED) {
|
||||||
|
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingEntry != null) {
|
if (existingEntry != null) {
|
||||||
// Replace the information in the existing entry
|
// Replace the information in the existing entry
|
||||||
existingEntry.details = details;
|
existingEntry.details = details;
|
||||||
@@ -617,8 +699,8 @@ 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) {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.limelight;
|
|
||||||
|
|
||||||
/* This is a dummy class to allow for a separate icon
|
|
||||||
* and launcher for TV.
|
|
||||||
*/
|
|
||||||
public class PcViewTv extends PcView {}
|
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
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 String appIdString;
|
||||||
|
private ArrayList<Intent> intentStack = new ArrayList<>();
|
||||||
|
|
||||||
|
private ComputerDetails computer;
|
||||||
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
|
||||||
|
public final static String APP_ID_EXTRA = "AppId";
|
||||||
|
|
||||||
|
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 (appIdString != null && appIdString.length() > 0) {
|
||||||
|
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
|
||||||
|
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
||||||
|
new NvApp("app", Integer.parseInt(appIdString), false), 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,
|
||||||
|
new NvApp("app", Integer.parseInt(appIdString), false), 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("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(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() {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
||||||
|
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
|
||||||
|
|
||||||
|
if (validateInput()) {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,86 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
private AudioTrack track;
|
private AudioTrack track;
|
||||||
|
|
||||||
@Override
|
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
||||||
public boolean streamInitialized(int channelCount, int channelMask, int samplesPerFrame, int sampleRate) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
int channelConfig;
|
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
int bufferSize;
|
48000,
|
||||||
int bytesPerFrame = (samplesPerFrame * 2);
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_GAME);
|
||||||
|
AudioFormat format = new AudioFormat.Builder()
|
||||||
|
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||||
|
.setSampleRate(48000)
|
||||||
|
.setChannelMask(channelConfig)
|
||||||
|
.build();
|
||||||
|
|
||||||
switch (channelCount)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
// Use FLAG_LOW_LATENCY on L through N
|
||||||
|
if (lowLatency) {
|
||||||
|
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
||||||
|
.setAudioFormat(format)
|
||||||
|
.setAudioAttributes(attributesBuilder.build())
|
||||||
|
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||||
|
.setBufferSizeInBytes(bufferSize);
|
||||||
|
|
||||||
|
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
||||||
|
if (lowLatency) {
|
||||||
|
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackBuilder.build();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new AudioTrack(attributesBuilder.build(),
|
||||||
|
format,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM,
|
||||||
|
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int setup(int audioConfiguration) {
|
||||||
|
int channelConfig;
|
||||||
|
int bytesPerFrame;
|
||||||
|
|
||||||
|
switch (audioConfiguration)
|
||||||
{
|
{
|
||||||
case 1:
|
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
break;
|
bytesPerFrame = 2 * 240 * 2;
|
||||||
case 2:
|
break;
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
break;
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
case 4:
|
bytesPerFrame = 6 * 240 * 2;
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
break;
|
||||||
break;
|
default:
|
||||||
case 6:
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
return -1;
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're not supposed to request less than the minimum
|
// We're not supposed to request less than the minimum
|
||||||
@@ -41,62 +88,102 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
|||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
// We'll try the small buffer size first and if it fails,
|
// We'll try the small buffer size first and if it fails,
|
||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
try {
|
|
||||||
// Buffer two frames of audio if possible
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
for (int i = 0; i < 4; i++) {
|
||||||
sampleRate,
|
boolean lowLatency;
|
||||||
channelConfig,
|
int bufferSize;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
track.play();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Try to release the AudioTrack if we got far enough
|
|
||||||
try {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
// Now try the larger buffer size
|
// We will try:
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
// 1) Small buffer, low latency mode
|
||||||
|
// 2) Large buffer, low latency mode
|
||||||
|
// 3) Small buffer, standard mode
|
||||||
|
// 4) Large buffer, standard mode
|
||||||
|
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
lowLatency = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
lowLatency = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
case 2:
|
||||||
|
bufferSize = bytesPerFrame * 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
case 3:
|
||||||
|
// Try the larger buffer size
|
||||||
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||||
sampleRate,
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||||
channelConfig,
|
continue;
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
}
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
try {
|
||||||
track.play();
|
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
||||||
|
track.play();
|
||||||
|
|
||||||
|
// Successfully created working AudioTrack. We're done here.
|
||||||
|
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try to release the AudioTrack if we got far enough
|
||||||
|
e.printStackTrace();
|
||||||
|
try {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
track = null;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
if (track == null) {
|
||||||
|
// Couldn't create any audio track for playback
|
||||||
return true;
|
return -2;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
|
||||||
track.write(audioData, offset, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void streamClosing() {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCapabilities() {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playDecodedAudio(byte[] audioData) {
|
||||||
|
track.write(audioData, 0, audioData.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
// Immediately drop all pending data
|
||||||
|
track.pause();
|
||||||
|
track.flush();
|
||||||
|
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("SHA1withRSA").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");
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,15 +2,12 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
import com.limelight.nvstream.input.KeycodeTranslator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to translate a Android key code into the codes GFE is expecting
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
* @author Diego Waxemberg
|
* @author Diego Waxemberg
|
||||||
* @author Cameron Gutman
|
* @author Cameron Gutman
|
||||||
*/
|
*/
|
||||||
public class KeyboardTranslator extends KeycodeTranslator {
|
public class KeyboardTranslator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GFE's prefix for every key code
|
* GFE's prefix for every key code
|
||||||
@@ -21,22 +18,15 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
public static final int VK_A = 65;
|
public static final int VK_A = 65;
|
||||||
public static final int VK_Z = 90;
|
public static final int VK_Z = 90;
|
||||||
public static final int VK_ALT = 18;
|
|
||||||
public static final int VK_NUMPAD0 = 96;
|
public static final int VK_NUMPAD0 = 96;
|
||||||
public static final int VK_BACK_SLASH = 92;
|
public static final int VK_BACK_SLASH = 92;
|
||||||
public static final int VK_CAPS_LOCK = 20;
|
public static final int VK_CAPS_LOCK = 20;
|
||||||
public static final int VK_CLEAR = 12;
|
public static final int VK_CLEAR = 12;
|
||||||
public static final int VK_COMMA = 44;
|
public static final int VK_COMMA = 44;
|
||||||
public static final int VK_CONTROL = 17;
|
|
||||||
public static final int VK_BACK_SPACE = 8;
|
public static final int VK_BACK_SPACE = 8;
|
||||||
public static final int VK_EQUALS = 61;
|
public static final int VK_EQUALS = 61;
|
||||||
public static final int VK_ESCAPE = 27;
|
public static final int VK_ESCAPE = 27;
|
||||||
public static final int VK_F1 = 112;
|
public static final int VK_F1 = 112;
|
||||||
public static final int VK_PERIOD = 46;
|
|
||||||
public static final int VK_INSERT = 155;
|
|
||||||
public static final int VK_OPEN_BRACKET = 91;
|
|
||||||
public static final int VK_WINDOWS = 524;
|
|
||||||
public static final int VK_MINUS = 45;
|
|
||||||
public static final int VK_END = 35;
|
public static final int VK_END = 35;
|
||||||
public static final int VK_HOME = 36;
|
public static final int VK_HOME = 36;
|
||||||
public static final int VK_NUM_LOCK = 144;
|
public static final int VK_NUM_LOCK = 144;
|
||||||
@@ -46,7 +36,6 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
public static final int VK_CLOSE_BRACKET = 93;
|
public static final int VK_CLOSE_BRACKET = 93;
|
||||||
public static final int VK_SCROLL_LOCK = 145;
|
public static final int VK_SCROLL_LOCK = 145;
|
||||||
public static final int VK_SEMICOLON = 59;
|
public static final int VK_SEMICOLON = 59;
|
||||||
public static final int VK_SHIFT = 16;
|
|
||||||
public static final int VK_SLASH = 47;
|
public static final int VK_SLASH = 47;
|
||||||
public static final int VK_SPACE = 32;
|
public static final int VK_SPACE = 32;
|
||||||
public static final int VK_PRINTSCREEN = 154;
|
public static final int VK_PRINTSCREEN = 154;
|
||||||
@@ -58,28 +47,32 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
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) {
|
||||||
* Constructs a new translator for the specified connection
|
switch (keycode)
|
||||||
* @param conn the connection to which the translated codes are sent
|
{
|
||||||
*/
|
case KeyEvent.KEYCODE_AT:
|
||||||
public KeyboardTranslator(NvConnection conn) {
|
case KeyEvent.KEYCODE_POUND:
|
||||||
super(conn);
|
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
|
||||||
* @return a GFE keycode for the given keycode
|
* @return a GFE keycode for the given keycode
|
||||||
*/
|
*/
|
||||||
@Override
|
public static short translate(int keycode) {
|
||||||
public short translate(int keycode) {
|
|
||||||
int translated;
|
int translated;
|
||||||
|
|
||||||
/* There seems to be no clean mapping between Android key codes
|
// This is a poor man's mapping between Android key codes
|
||||||
* and what Nvidia sends over the wire. If someone finds one,
|
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||||
* I'll happily delete this code :)
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||||
*/
|
|
||||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||||
keycode <= KeyEvent.KEYCODE_9) {
|
keycode <= KeyEvent.KEYCODE_9) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||||
@@ -99,8 +92,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
else {
|
else {
|
||||||
switch (keycode) {
|
switch (keycode) {
|
||||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
|
translated = 0xA4;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
translated = VK_ALT;
|
translated = 0xA5;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BACKSLASH:
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
@@ -120,8 +116,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
|
translated = 0xA2;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
translated = VK_CONTROL;
|
translated = 0xA3;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DEL:
|
case KeyEvent.KEYCODE_DEL:
|
||||||
@@ -131,7 +130,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
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;
|
||||||
@@ -141,23 +141,25 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
// Nvidia maps period to delete
|
translated = 0x2e;
|
||||||
translated = VK_PERIOD;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_INSERT:
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
translated = -1;
|
translated = 0x2d;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||||
translated = 0xdb;
|
translated = 0xdb;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_META_LEFT:
|
case KeyEvent.KEYCODE_META_LEFT:
|
||||||
case KeyEvent.KEYCODE_META_RIGHT:
|
translated = 0x5b;
|
||||||
translated = VK_WINDOWS;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
|
translated = 0x5c;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MINUS:
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
translated = 0xbd;
|
translated = 0xbd;
|
||||||
break;
|
break;
|
||||||
@@ -199,8 +201,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
|
translated = 0xA0;
|
||||||
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
translated = VK_SHIFT;
|
translated = 0xA1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SLASH:
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
@@ -247,7 +252,39 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
case KeyEvent.KEYCODE_BREAK:
|
case KeyEvent.KEYCODE_BREAK:
|
||||||
translated = VK_PAUSE;
|
translated = VK_PAUSE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||||
|
translated = 0x6F;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||||
|
translated = 0x6A;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||||
|
translated = 0x6D;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||||
|
translated = 0x6B;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||||
|
translated = 0x6E;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_AT:
|
||||||
|
translated = 2 + VK_0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_POUND:
|
||||||
|
translated = 3 + VK_0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyEvent.KEYCODE_STAR:
|
||||||
|
translated = 8 + VK_0;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
System.out.println("No key for "+keycode);
|
System.out.println("No key for "+keycode);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
@@ -17,23 +19,27 @@ public class TouchContext {
|
|||||||
private boolean confirmedDrag;
|
private boolean confirmedDrag;
|
||||||
private Timer dragTimer;
|
private Timer dragTimer;
|
||||||
private double distanceMoved;
|
private double distanceMoved;
|
||||||
|
private double xFactor, yFactor;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private final NvConnection conn;
|
||||||
private final int actionIndex;
|
private final int actionIndex;
|
||||||
private final double xFactor;
|
private final int referenceWidth;
|
||||||
private final double yFactor;
|
private final int referenceHeight;
|
||||||
|
private final View targetView;
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
public TouchContext(NvConnection conn, int actionIndex,
|
||||||
|
int referenceWidth, int referenceHeight, View view)
|
||||||
{
|
{
|
||||||
this.conn = conn;
|
this.conn = conn;
|
||||||
this.actionIndex = actionIndex;
|
this.actionIndex = actionIndex;
|
||||||
this.xFactor = xFactor;
|
this.referenceWidth = referenceWidth;
|
||||||
this.yFactor = yFactor;
|
this.referenceHeight = referenceHeight;
|
||||||
|
this.targetView = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getActionIndex()
|
public int getActionIndex()
|
||||||
@@ -68,6 +74,10 @@ public class TouchContext {
|
|||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
|
// Get the view dimensions to scale inputs on this touch
|
||||||
|
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||||
|
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
originalTouchX = lastTouchX = eventX;
|
||||||
originalTouchY = lastTouchY = eventY;
|
originalTouchY = lastTouchY = eventY;
|
||||||
originalTouchTime = System.currentTimeMillis();
|
originalTouchTime = System.currentTimeMillis();
|
||||||
|
|||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
||||||
|
|
||||||
|
private View targetView;
|
||||||
|
|
||||||
|
public AndroidNativePointerCaptureProvider(View targetView) {
|
||||||
|
this.targetView = targetView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
|
targetView.requestPointerCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
|
targetView.releasePointerCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCapturingActive() {
|
||||||
|
return targetView.hasPointerCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
float x = event.getX();
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
x += event.getHistoricalX(i);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
float y = event.getY();
|
||||||
|
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||||
|
y += event.getHistoricalY(i);
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.PointerIcon;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||||
|
private ViewGroup rootViewGroup;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public AndroidPointerIconCaptureProvider(Activity activity) {
|
||||||
|
this.context = activity;
|
||||||
|
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPointerIconOnAllViews(PointerIcon icon) {
|
||||||
|
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
||||||
|
View view = rootViewGroup.getChildAt(i);
|
||||||
|
view.setPointerIcon(icon);
|
||||||
|
}
|
||||||
|
rootViewGroup.setPointerIcon(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
|
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
|
setPointerIconOnAllViews(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
||||||
|
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
|
||||||
|
import com.limelight.binding.input.evdev.EvdevListener;
|
||||||
|
|
||||||
|
public class InputCaptureManager {
|
||||||
|
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||||
|
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Android O+ native mouse capture");
|
||||||
|
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||||
|
}
|
||||||
|
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
||||||
|
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
||||||
|
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||||
|
return new ShieldCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
||||||
|
LimeLog.info("Using Evdev mouse capture");
|
||||||
|
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
|
||||||
|
}
|
||||||
|
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
||||||
|
// Android N's native capture can't capture over system UI elements
|
||||||
|
// so we want to only use it if there's no other option.
|
||||||
|
LimeLog.info("Using Android N+ pointer hiding");
|
||||||
|
return new AndroidPointerIconCaptureProvider(activity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Mouse capture not available");
|
||||||
|
return new NullCaptureProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
public abstract class InputCaptureProvider {
|
||||||
|
protected boolean isCapturing;
|
||||||
|
|
||||||
|
public void enableCapture() {
|
||||||
|
isCapturing = true;
|
||||||
|
}
|
||||||
|
public void disableCapture() {
|
||||||
|
isCapturing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void destroy() {}
|
||||||
|
|
||||||
|
public boolean isCapturingEnabled() {
|
||||||
|
return isCapturing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCapturingActive() {
|
||||||
|
return isCapturing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
|
public class NullCaptureProvider extends InputCaptureProvider {}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.input.InputManager;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
||||||
|
// mode without having to grab the input device (which requires root). The data comes in the form
|
||||||
|
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
||||||
|
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
||||||
|
//
|
||||||
|
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
||||||
|
|
||||||
|
public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||||
|
private static boolean nvExtensionSupported;
|
||||||
|
private static Method methodSetCursorVisibility;
|
||||||
|
private static int AXIS_RELATIVE_X;
|
||||||
|
private static int AXIS_RELATIVE_Y;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
||||||
|
|
||||||
|
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
||||||
|
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
||||||
|
|
||||||
|
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
||||||
|
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
||||||
|
|
||||||
|
nvExtensionSupported = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
nvExtensionSupported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShieldCaptureProvider(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return nvExtensionSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setCursorVisibility(boolean visible) {
|
||||||
|
try {
|
||||||
|
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
|
setCursorVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
|
setCursorVisibility(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||||
|
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisX(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getRelativeAxisY(MotionEvent event) {
|
||||||
|
return event.getAxisValue(AXIS_RELATIVE_Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public abstract class AbstractController {
|
||||||
|
|
||||||
|
private final int deviceId;
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
|
||||||
|
protected short buttonFlags;
|
||||||
|
protected float leftTrigger, rightTrigger;
|
||||||
|
protected float rightStickX, rightStickY;
|
||||||
|
protected float leftStickX, leftStickY;
|
||||||
|
|
||||||
|
public int getControllerId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setButtonFlag(int buttonFlag, int data) {
|
||||||
|
if (data != 0) {
|
||||||
|
buttonFlags |= buttonFlag;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonFlags &= ~buttonFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void reportInput() {
|
||||||
|
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||||
|
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean start();
|
||||||
|
public abstract void stop();
|
||||||
|
|
||||||
|
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||||
|
|
||||||
|
protected void notifyDeviceRemoved() {
|
||||||
|
listener.deviceRemoved(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceAdded() {
|
||||||
|
listener.deviceAdded(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
import android.hardware.usb.UsbEndpoint;
|
||||||
|
import android.hardware.usb.UsbInterface;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
public abstract class AbstractXboxController extends AbstractController {
|
||||||
|
protected final UsbDevice device;
|
||||||
|
protected final UsbDeviceConnection connection;
|
||||||
|
|
||||||
|
private Thread inputThread;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
protected UsbEndpoint inEndpt, outEndpt;
|
||||||
|
|
||||||
|
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(deviceId, listener);
|
||||||
|
this.device = device;
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread createInputThread() {
|
||||||
|
return new Thread() {
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted() && !stopped) {
|
||||||
|
byte[] buffer = new byte[64];
|
||||||
|
|
||||||
|
int res;
|
||||||
|
|
||||||
|
//
|
||||||
|
// There's no way that I can tell to determine if a device has failed
|
||||||
|
// or if the timeout has simply expired. We'll check how long the transfer
|
||||||
|
// took to fail and assume the device failed if it happened before the timeout
|
||||||
|
// expired.
|
||||||
|
//
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Read the next input state packet
|
||||||
|
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||||
|
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||||
|
|
||||||
|
// If we get a zero length response, treat it as an error
|
||||||
|
if (res == 0) {
|
||||||
|
res = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||||
|
LimeLog.warning("Detected device I/O error");
|
||||||
|
AbstractXboxController.this.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (res == -1 && !isInterrupted() && !stopped);
|
||||||
|
|
||||||
|
if (res == -1 || stopped) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
||||||
|
// Report input if handleRead() returns true
|
||||||
|
reportInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean start() {
|
||||||
|
// Force claim all interfaces
|
||||||
|
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||||
|
UsbInterface iface = device.getInterface(i);
|
||||||
|
|
||||||
|
if (!connection.claimInterface(iface, true)) {
|
||||||
|
LimeLog.warning("Failed to claim interfaces");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the endpoints
|
||||||
|
UsbInterface iface = device.getInterface(0);
|
||||||
|
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||||
|
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||||
|
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||||
|
if (inEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate IN endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inEndpt = endpt;
|
||||||
|
}
|
||||||
|
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||||
|
if (outEndpt != null) {
|
||||||
|
LimeLog.warning("Found duplicate OUT endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outEndpt = endpt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the required endpoints were present
|
||||||
|
if (inEndpt == null || outEndpt == null) {
|
||||||
|
LimeLog.warning("Missing required endpoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the init function
|
||||||
|
if (!doInit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report that we're added _before_ starting the input thread
|
||||||
|
notifyDeviceAdded();
|
||||||
|
|
||||||
|
// Start listening for controller input
|
||||||
|
inputThread = createInputThread();
|
||||||
|
inputThread.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped = true;
|
||||||
|
|
||||||
|
// Cancel any rumble effects
|
||||||
|
rumble((short)0, (short)0);
|
||||||
|
|
||||||
|
// Stop the input thread
|
||||||
|
if (inputThread != null) {
|
||||||
|
inputThread.interrupt();
|
||||||
|
inputThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the USB connection
|
||||||
|
connection.close();
|
||||||
|
|
||||||
|
// Report the device removed
|
||||||
|
notifyDeviceRemoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||||
|
protected abstract boolean doInit();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ import android.hardware.usb.UsbDevice;
|
|||||||
import android.hardware.usb.UsbDeviceConnection;
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
import android.hardware.usb.UsbManager;
|
import android.hardware.usb.UsbManager;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@@ -20,14 +28,15 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
"com.limelight.USB_PERMISSION";
|
"com.limelight.USB_PERMISSION";
|
||||||
|
|
||||||
private UsbManager usbManager;
|
private UsbManager usbManager;
|
||||||
|
private PreferenceConfiguration prefConfig;
|
||||||
|
|
||||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||||
|
|
||||||
private final ArrayList<XboxOneController> controllers = new ArrayList<>();
|
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
private UsbDriverListener listener;
|
||||||
private static int nextDeviceId;
|
private int nextDeviceId;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
||||||
@@ -38,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 (XboxOneController 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,14 +72,26 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Initial attachment broadcast
|
// Initial attachment broadcast
|
||||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// Continue the state machine
|
// shouldClaimDevice() looks at the kernel's enumerated input
|
||||||
handleUsbDeviceState(device);
|
// devices to make its decision about whether to prompt to take
|
||||||
|
// control of the device. The kernel bringing up the input stack
|
||||||
|
// may race with this callback and cause us to prompt when the
|
||||||
|
// kernel is capable of running the device. Let's post a delayed
|
||||||
|
// message to process this state change to allow the kernel
|
||||||
|
// some time to bring up the stack.
|
||||||
|
new Handler().postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Continue the state machine
|
||||||
|
handleUsbDeviceState(device);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
// Subsequent permission dialog completion intent
|
// Subsequent permission dialog completion intent
|
||||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
|
|
||||||
// If we got this far, we've already found we're able to handle this device
|
// If we got this far, we've already found we're able to handle this device
|
||||||
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||||
@@ -91,8 +107,8 @@ 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 (XboxOneController controller : controllers) {
|
for (AbstractController controller : controllers) {
|
||||||
listener.deviceAdded(controller.getControllerId());
|
listener.deviceAdded(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,19 +116,46 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
private void handleUsbDeviceState(UsbDevice device) {
|
||||||
// Are we able to operate it?
|
// Are we able to operate it?
|
||||||
if (XboxOneController.canClaimDevice(device)) {
|
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
||||||
// Do we have permission yet?
|
// Do we have permission yet?
|
||||||
if (!usbManager.hasPermission(device)) {
|
if (!usbManager.hasPermission(device)) {
|
||||||
// Let's ask for permission
|
// Let's ask for permission
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
try {
|
||||||
|
// This function is not documented as throwing any exceptions (denying access
|
||||||
|
// is indicated by calling the PendingIntent with a false result). However,
|
||||||
|
// Samsung Knox has some policies which block this request, but rather than
|
||||||
|
// just returning a false result or returning 0 enumerated devices,
|
||||||
|
// they throw an undocumented SecurityException from this call, crashing
|
||||||
|
// the whole app. :(
|
||||||
|
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the device
|
// Open the device
|
||||||
UsbDeviceConnection connection = usbManager.openDevice(device);
|
UsbDeviceConnection connection = usbManager.openDevice(device);
|
||||||
|
if (connection == null) {
|
||||||
|
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to initialize it
|
|
||||||
XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
AbstractController controller;
|
||||||
|
|
||||||
|
if (XboxOneController.canClaimDevice(device)) {
|
||||||
|
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||||
|
}
|
||||||
|
else if (Xbox360Controller.canClaimDevice(device)) {
|
||||||
|
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Unreachable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the controller
|
||||||
if (!controller.start()) {
|
if (!controller.start()) {
|
||||||
connection.close();
|
connection.close();
|
||||||
return;
|
return;
|
||||||
@@ -123,9 +166,42 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
||||||
|
// On KitKat and later, we can determine if this VID and PID combo
|
||||||
|
// matches an existing input device and defer to the built-in controller
|
||||||
|
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
for (int id : InputDevice.getDeviceIds()) {
|
||||||
|
InputDevice inputDev = InputDevice.getDevice(id);
|
||||||
|
if (inputDev == null) {
|
||||||
|
// Device was removed while looping
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||||
|
inputDev.getProductId() == device.getProductId()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||||
|
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||||
|
// if we know the kernel isn't already driving this device.
|
||||||
|
return XboxOneController.canClaimDevice(device) ||
|
||||||
|
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||||
|
this.prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||||
|
|
||||||
// Register for USB attach broadcasts and permission completions
|
// Register for USB attach broadcasts and permission completions
|
||||||
IntentFilter filter = new IntentFilter();
|
IntentFilter filter = new IntentFilter();
|
||||||
@@ -135,7 +211,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
|||||||
|
|
||||||
// Enumerate existing devices
|
// Enumerate existing devices
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
if (XboxOneController.canClaimDevice(dev)) {
|
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
|
||||||
// Start the process of claiming this device
|
// Start the process of claiming this device
|
||||||
handleUsbDeviceState(dev);
|
handleUsbDeviceState(dev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.hardware.usb.UsbConstants;
|
||||||
|
import android.hardware.usb.UsbDevice;
|
||||||
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class Xbox360Controller extends AbstractXboxController {
|
||||||
|
private static final int XB360_IFACE_SUBCLASS = 93;
|
||||||
|
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||||
|
|
||||||
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
|
0x0079, // GPD Win 2
|
||||||
|
0x044f, // Thrustmaster
|
||||||
|
0x045e, // Microsoft
|
||||||
|
0x046d, // Logitech
|
||||||
|
0x056e, // Elecom
|
||||||
|
0x06a3, // Saitek
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x07ff, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1038, // SteelSeries
|
||||||
|
0x11c9, // Nacon
|
||||||
|
0x12ab, // Unknown
|
||||||
|
0x1430, // RedOctane
|
||||||
|
0x146b, // BigBen
|
||||||
|
0x1532, // Razer Sabertooth
|
||||||
|
0x15e4, // Numark
|
||||||
|
0x162e, // Joytech
|
||||||
|
0x1689, // Razer Onza
|
||||||
|
0x1bad, // Harmonix
|
||||||
|
0x24c6, // PowerA
|
||||||
|
};
|
||||||
|
|
||||||
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
|
if (device.getVendorId() == supportedVid &&
|
||||||
|
device.getInterfaceCount() >= 1 &&
|
||||||
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||||
|
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||||
|
super(device, connection, deviceId, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int unsignByte(byte b) {
|
||||||
|
if (b < 0) {
|
||||||
|
return b + 256;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
|
if (buffer.limit() < 14) {
|
||||||
|
LimeLog.severe("Read too small: "+buffer.limit());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip first short
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
|
||||||
|
// DPAD
|
||||||
|
byte b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
||||||
|
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
||||||
|
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
// Start/Select
|
||||||
|
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
||||||
|
|
||||||
|
// LS/RS
|
||||||
|
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
// ABXY buttons
|
||||||
|
b = buffer.get();
|
||||||
|
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||||
|
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
// LB/RB
|
||||||
|
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
||||||
|
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
||||||
|
|
||||||
|
// Xbox button
|
||||||
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
||||||
|
|
||||||
|
// Triggers
|
||||||
|
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||||
|
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||||
|
|
||||||
|
// Left stick
|
||||||
|
leftStickX = buffer.getShort() / 32767.0f;
|
||||||
|
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
// Right stick
|
||||||
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
// Return true to send input
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendLedCommand(byte command) {
|
||||||
|
byte[] commandBuffer = {0x01, 0x03, command};
|
||||||
|
|
||||||
|
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
||||||
|
if (res != commandBuffer.length) {
|
||||||
|
LimeLog.warning("LED set transfer failed: "+res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doInit() {
|
||||||
|
// Turn the LED on corresponding to our device ID
|
||||||
|
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
||||||
|
|
||||||
|
// No need to fail init if the LED command fails
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,60 +3,55 @@ package com.limelight.binding.input.driver;
|
|||||||
import android.hardware.usb.UsbConstants;
|
import android.hardware.usb.UsbConstants;
|
||||||
import android.hardware.usb.UsbDevice;
|
import android.hardware.usb.UsbDevice;
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
import android.hardware.usb.UsbDeviceConnection;
|
||||||
import android.hardware.usb.UsbEndpoint;
|
|
||||||
import android.hardware.usb.UsbInterface;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class XboxOneController {
|
public class XboxOneController extends AbstractXboxController {
|
||||||
private final UsbDevice device;
|
|
||||||
private final UsbDeviceConnection connection;
|
|
||||||
private final int deviceId;
|
|
||||||
|
|
||||||
private Thread inputThread;
|
|
||||||
private UsbDriverListener listener;
|
|
||||||
private boolean stopped;
|
|
||||||
|
|
||||||
private short buttonFlags;
|
|
||||||
private float leftTrigger, rightTrigger;
|
|
||||||
private float rightStickX, rightStickY;
|
|
||||||
private float leftStickX, leftStickY;
|
|
||||||
|
|
||||||
private static final int MICROSOFT_VID = 0x045e;
|
|
||||||
private static final int XB1_IFACE_SUBCLASS = 71;
|
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||||
private static final int XB1_IFACE_PROTOCOL = 208;
|
private static final int XB1_IFACE_PROTOCOL = 208;
|
||||||
|
|
||||||
// FIXME: odata_serial
|
private static final int[] SUPPORTED_VENDORS = {
|
||||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
0x045e, // Microsoft
|
||||||
|
0x0738, // Mad Catz
|
||||||
|
0x0e6f, // Unknown
|
||||||
|
0x0f0d, // Hori
|
||||||
|
0x1532, // Razer Wildcat
|
||||||
|
0x24c6, // PowerA
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final byte[] FW2015_INIT = {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) {
|
||||||
this.device = device;
|
super(device, connection, deviceId, listener);
|
||||||
this.connection = connection;
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getControllerId() {
|
|
||||||
return this.deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setButtonFlag(int buttonFlag, int data) {
|
|
||||||
if (data != 0) {
|
|
||||||
buttonFlags |= buttonFlag;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buttonFlags &= ~buttonFlag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportInput() {
|
|
||||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
|
||||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processButtons(ByteBuffer buffer) {
|
private void processButtons(ByteBuffer buffer) {
|
||||||
@@ -90,144 +85,105 @@ public class XboxOneController {
|
|||||||
|
|
||||||
rightStickX = buffer.getShort() / 32767.0f;
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
reportInput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processPacket(ByteBuffer buffer) {
|
private void ackModeReport(byte seqNum) {
|
||||||
|
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean handleRead(ByteBuffer buffer) {
|
||||||
switch (buffer.get())
|
switch (buffer.get())
|
||||||
{
|
{
|
||||||
case 0x20:
|
case 0x20:
|
||||||
buffer.position(buffer.position()+3);
|
buffer.position(buffer.position()+3);
|
||||||
processButtons(buffer);
|
processButtons(buffer);
|
||||||
break;
|
return true;
|
||||||
|
|
||||||
case 0x07:
|
case 0x07:
|
||||||
buffer.position(buffer.position() + 3);
|
// The Xbox One S controller needs acks for mode reports otherwise
|
||||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
// it retransmits them forever.
|
||||||
reportInput();
|
if (buffer.get() == 0x30) {
|
||||||
break;
|
ackModeReport(buffer.get());
|
||||||
}
|
buffer.position(buffer.position() + 1);
|
||||||
}
|
|
||||||
|
|
||||||
private void startInputThread(final UsbEndpoint inEndpt) {
|
|
||||||
inputThread = new Thread() {
|
|
||||||
public void run() {
|
|
||||||
while (!isInterrupted() && !stopped) {
|
|
||||||
byte[] buffer = new byte[64];
|
|
||||||
|
|
||||||
int res;
|
|
||||||
|
|
||||||
//
|
|
||||||
// There's no way that I can tell to determine if a device has failed
|
|
||||||
// or if the timeout has simply expired. We'll check how long the transfer
|
|
||||||
// took to fail and assume the device failed if it happened before the timeout
|
|
||||||
// expired.
|
|
||||||
//
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Read the next input state packet
|
|
||||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
|
||||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
|
||||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
|
||||||
LimeLog.warning("Detected device I/O error");
|
|
||||||
XboxOneController.this.stop();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (res == -1 && !isInterrupted() && !stopped);
|
|
||||||
|
|
||||||
if (res == -1 || stopped) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
|
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
};
|
buffer.position(buffer.position() + 2);
|
||||||
inputThread.setName("Xbox One Controller - Input Thread");
|
}
|
||||||
inputThread.start();
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean start() {
|
public static boolean canClaimDevice(UsbDevice device) {
|
||||||
// Force claim all interfaces
|
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
if (device.getVendorId() == supportedVid &&
|
||||||
UsbInterface iface = device.getInterface(i);
|
device.getInterfaceCount() >= 1 &&
|
||||||
|
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||||
|
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||||
|
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!connection.claimInterface(iface, true)) {
|
return false;
|
||||||
LimeLog.warning("Failed to claim interfaces");
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doInit() {
|
||||||
|
// Send all applicable init packets
|
||||||
|
for (InitPacket pkt : INIT_PKTS) {
|
||||||
|
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
|
||||||
|
|
||||||
|
// Populate sequence number
|
||||||
|
data[2] = seqNum++;
|
||||||
|
|
||||||
|
// Send the initialization packet
|
||||||
|
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
|
||||||
|
if (res != data.length) {
|
||||||
|
LimeLog.warning("Initialization transfer failed: "+res);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the endpoints
|
|
||||||
UsbEndpoint outEndpt = null;
|
|
||||||
UsbEndpoint inEndpt = null;
|
|
||||||
UsbInterface iface = device.getInterface(0);
|
|
||||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
|
||||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
|
||||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
|
||||||
if (inEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate IN endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
inEndpt = endpt;
|
|
||||||
}
|
|
||||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
|
||||||
if (outEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate OUT endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
outEndpt = endpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the required endpoints were present
|
|
||||||
if (inEndpt == null || outEndpt == null) {
|
|
||||||
LimeLog.warning("Missing required endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the initialization packet
|
|
||||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
|
||||||
if (res != XB1_INIT_DATA.length) {
|
|
||||||
LimeLog.warning("Initialization transfer failed: "+res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start listening for controller input
|
|
||||||
startInputThread(inEndpt);
|
|
||||||
|
|
||||||
// Report this device added via the listener
|
|
||||||
listener.deviceAdded(deviceId);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
@Override
|
||||||
if (stopped) {
|
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopped = true;
|
|
||||||
|
|
||||||
// Stop the input thread
|
|
||||||
if (inputThread != null) {
|
|
||||||
inputThread.interrupt();
|
|
||||||
inputThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report the device removed
|
|
||||||
listener.deviceRemoved(deviceId);
|
|
||||||
|
|
||||||
// Close the USB connection
|
|
||||||
connection.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
private static class InitPacket {
|
||||||
return device.getVendorId() == MICROSOFT_VID &&
|
final int vendorId;
|
||||||
device.getInterfaceCount() >= 1 &&
|
final int productId;
|
||||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
final byte[] data;
|
||||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
|
||||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
|
InitPacket(int vendorId, int productId, byte[] data) {
|
||||||
|
this.vendorId = vendorId;
|
||||||
|
this.productId = productId;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.limelight.LimelightBuildProps;
|
||||||
|
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||||
|
|
||||||
|
public class EvdevCaptureProviderShim {
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return LimelightBuildProps.ROOT_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to construct our capture provider using reflection because it isn't included in non-root builds
|
||||||
|
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
||||||
|
try {
|
||||||
|
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
|
||||||
|
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class EvdevHandler {
|
|
||||||
|
|
||||||
private final EvdevListener listener;
|
|
||||||
private final String libraryPath;
|
|
||||||
|
|
||||||
private boolean shutdown = false;
|
|
||||||
private InputStream evdevIn;
|
|
||||||
private OutputStream evdevOut;
|
|
||||||
private Process reader;
|
|
||||||
|
|
||||||
private static final byte UNGRAB_REQUEST = 1;
|
|
||||||
private static final byte REGRAB_REQUEST = 2;
|
|
||||||
|
|
||||||
private final Thread handlerThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
int deltaX = 0;
|
|
||||||
int deltaY = 0;
|
|
||||||
byte deltaScroll = 0;
|
|
||||||
|
|
||||||
// Launch the evdev reader shell
|
|
||||||
ProcessBuilder builder = new ProcessBuilder("su", "-c", libraryPath+File.separatorChar+"libevdev_reader.so");
|
|
||||||
builder.redirectErrorStream(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
reader = builder.start();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
evdevIn = reader.getInputStream();
|
|
||||||
evdevOut = reader.getOutputStream();
|
|
||||||
|
|
||||||
while (!isInterrupted() && !shutdown) {
|
|
||||||
EvdevEvent event;
|
|
||||||
try {
|
|
||||||
event = EvdevReader.read(evdevIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
event = null;
|
|
||||||
}
|
|
||||||
if (event == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case EvdevEvent.EV_SYN:
|
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
|
||||||
listener.mouseMove(deltaX, deltaY);
|
|
||||||
deltaX = deltaY = 0;
|
|
||||||
}
|
|
||||||
if (deltaScroll != 0) {
|
|
||||||
listener.mouseScroll(deltaScroll);
|
|
||||||
deltaScroll = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.EV_REL:
|
|
||||||
switch (event.code) {
|
|
||||||
case EvdevEvent.REL_X:
|
|
||||||
deltaX = event.value;
|
|
||||||
break;
|
|
||||||
case EvdevEvent.REL_Y:
|
|
||||||
deltaY = event.value;
|
|
||||||
break;
|
|
||||||
case EvdevEvent.REL_WHEEL:
|
|
||||||
deltaScroll = (byte) event.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.EV_KEY:
|
|
||||||
switch (event.code) {
|
|
||||||
case EvdevEvent.BTN_LEFT:
|
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
|
||||||
event.value != 0);
|
|
||||||
break;
|
|
||||||
case EvdevEvent.BTN_MIDDLE:
|
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
|
||||||
event.value != 0);
|
|
||||||
break;
|
|
||||||
case EvdevEvent.BTN_RIGHT:
|
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
|
||||||
event.value != 0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.BTN_SIDE:
|
|
||||||
case EvdevEvent.BTN_EXTRA:
|
|
||||||
case EvdevEvent.BTN_FORWARD:
|
|
||||||
case EvdevEvent.BTN_BACK:
|
|
||||||
case EvdevEvent.BTN_TASK:
|
|
||||||
// Other unhandled mouse buttons
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// We got some unrecognized button. This means
|
|
||||||
// someone is trying to use the other device in this
|
|
||||||
// "combination" input device. We'll try to handle
|
|
||||||
// it via keyboard, but we're not going to disconnect
|
|
||||||
// if we can't
|
|
||||||
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
|
||||||
if (keyCode != 0) {
|
|
||||||
listener.keyboardEvent(event.value != 0, keyCode);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.EV_MSC:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public EvdevHandler(Context context, EvdevListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
this.libraryPath = context.getApplicationInfo().nativeLibraryDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void regrabAll() {
|
|
||||||
if (!shutdown && evdevOut != null) {
|
|
||||||
try {
|
|
||||||
evdevOut.write(REGRAB_REQUEST);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ungrabAll() {
|
|
||||||
if (!shutdown && evdevOut != null) {
|
|
||||||
try {
|
|
||||||
evdevOut.write(UNGRAB_REQUEST);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
handlerThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
// We need to stop the process in this context otherwise
|
|
||||||
// we could get stuck waiting on output from the process
|
|
||||||
// in order to terminate it.
|
|
||||||
|
|
||||||
shutdown = true;
|
|
||||||
handlerThread.interrupt();
|
|
||||||
|
|
||||||
if (evdevIn != null) {
|
|
||||||
try {
|
|
||||||
evdevIn.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evdevOut != null) {
|
|
||||||
try {
|
|
||||||
evdevOut.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader != null) {
|
|
||||||
reader.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handlerThread.join();
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public interface EvdevListener {
|
public interface EvdevListener {
|
||||||
public static final int BUTTON_LEFT = 1;
|
int BUTTON_LEFT = 1;
|
||||||
public static final int BUTTON_MIDDLE = 2;
|
int BUTTON_MIDDLE = 2;
|
||||||
public static final int BUTTON_RIGHT = 3;
|
int BUTTON_RIGHT = 3;
|
||||||
|
int BUTTON_X1 = 4;
|
||||||
|
int BUTTON_X2 = 5;
|
||||||
|
|
||||||
public void mouseMove(int deltaX, int deltaY);
|
void mouseMove(int deltaX, int deltaY);
|
||||||
public void mouseButtonEvent(int buttonId, boolean down);
|
void mouseButtonEvent(int buttonId, boolean down);
|
||||||
public void mouseScroll(byte amount);
|
void mouseScroll(byte amount);
|
||||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,350 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
||||||
|
*/
|
||||||
|
public class AnalogStick extends VirtualControllerElement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* outer radius size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_COMPLETE = 90;
|
||||||
|
/**
|
||||||
|
* analog stick size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
||||||
|
/**
|
||||||
|
* dead zone size in percent of the ui element
|
||||||
|
*/
|
||||||
|
public static final int SIZE_RADIUS_DEADZONE = 90;
|
||||||
|
/**
|
||||||
|
* time frame for a double click
|
||||||
|
*/
|
||||||
|
public final static long timeoutDoubleClick = 350;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
||||||
|
*/
|
||||||
|
public final static long timeoutDeadzone = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface to update registered observers.
|
||||||
|
*/
|
||||||
|
public interface AnalogStickListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
||||||
|
*
|
||||||
|
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
||||||
|
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
||||||
|
*/
|
||||||
|
void onMovement(float x, float y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClick event will be fired on click on the analog stick
|
||||||
|
*/
|
||||||
|
void onClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
||||||
|
* stick.
|
||||||
|
*/
|
||||||
|
void onDoubleClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRevoke event will be fired on unpress of the analog stick.
|
||||||
|
*/
|
||||||
|
void onRevoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Movement states of the analog sick.
|
||||||
|
*/
|
||||||
|
private enum STICK_STATE {
|
||||||
|
NO_MOVEMENT,
|
||||||
|
MOVED_IN_DEAD_ZONE,
|
||||||
|
MOVED_ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click type states.
|
||||||
|
*/
|
||||||
|
private enum CLICK_STATE {
|
||||||
|
SINGLE,
|
||||||
|
DOUBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* configuration if the analog stick should be displayed as circle or square
|
||||||
|
*/
|
||||||
|
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* outer radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_complete = 0;
|
||||||
|
/**
|
||||||
|
* analog stick radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_analog_stick = 0;
|
||||||
|
/**
|
||||||
|
* dead zone radius, this size will be automatically updated on resize
|
||||||
|
*/
|
||||||
|
private float radius_dead_zone = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* horizontal position in relation to the center of the element
|
||||||
|
*/
|
||||||
|
private float relative_x = 0;
|
||||||
|
/**
|
||||||
|
* vertical position in relation to the center of the element
|
||||||
|
*/
|
||||||
|
private float relative_y = 0;
|
||||||
|
|
||||||
|
|
||||||
|
private double movement_radius = 0;
|
||||||
|
private double movement_angle = 0;
|
||||||
|
|
||||||
|
private float position_stick_x = 0;
|
||||||
|
private float position_stick_y = 0;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
|
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
||||||
|
|
||||||
|
private List<AnalogStickListener> listeners = new ArrayList<>();
|
||||||
|
private long timeLastClick = 0;
|
||||||
|
|
||||||
|
private static double getMovementRadius(float x, float y) {
|
||||||
|
return Math.sqrt(x * x + y * y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double getAngle(float way_x, float way_y) {
|
||||||
|
// prevent divisions by zero for corner cases
|
||||||
|
if (way_x == 0) {
|
||||||
|
return way_y < 0 ? Math.PI : 0;
|
||||||
|
} else if (way_y == 0) {
|
||||||
|
if (way_x > 0) {
|
||||||
|
return Math.PI * 3 / 2;
|
||||||
|
} else if (way_x < 0) {
|
||||||
|
return Math.PI * 1 / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return correct calculated angle for each quadrant
|
||||||
|
if (way_x > 0) {
|
||||||
|
if (way_y < 0) {
|
||||||
|
// first quadrant
|
||||||
|
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
||||||
|
} else {
|
||||||
|
// second quadrant
|
||||||
|
return Math.PI + Math.atan((double) (way_x / way_y));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (way_y > 0) {
|
||||||
|
// third quadrant
|
||||||
|
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
||||||
|
} else {
|
||||||
|
// fourth quadrant
|
||||||
|
return 0 + Math.atan((double) (-way_x / -way_y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnalogStick(VirtualController controller, Context context, int elementId) {
|
||||||
|
super(controller, context, elementId);
|
||||||
|
// reset stick position
|
||||||
|
position_stick_x = getWidth() / 2;
|
||||||
|
position_stick_y = getHeight() / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAnalogStickListener(AnalogStickListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnMovement(float x, float y) {
|
||||||
|
_DBG("movement x: " + x + " movement y: " + y);
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onMovement(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnClick() {
|
||||||
|
_DBG("click");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnDoubleClick() {
|
||||||
|
_DBG("double click");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onDoubleClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyOnRevoke() {
|
||||||
|
_DBG("revoke");
|
||||||
|
// notify listeners
|
||||||
|
for (AnalogStickListener listener : listeners) {
|
||||||
|
listener.onRevoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
// calculate new radius sizes depending
|
||||||
|
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
||||||
|
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||||
|
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||||
|
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
// draw outer circle
|
||||||
|
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
} else {
|
||||||
|
paint.setColor(pressedColor);
|
||||||
|
}
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
||||||
|
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
// draw dead zone
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
||||||
|
|
||||||
|
// draw stick depending on state
|
||||||
|
switch (stick_state) {
|
||||||
|
case NO_MOVEMENT: {
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MOVED_IN_DEAD_ZONE:
|
||||||
|
case MOVED_ACTIVE: {
|
||||||
|
paint.setColor(pressedColor);
|
||||||
|
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePosition() {
|
||||||
|
// get 100% way
|
||||||
|
float complete = radius_complete - radius_analog_stick;
|
||||||
|
|
||||||
|
// calculate relative way
|
||||||
|
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||||
|
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||||
|
|
||||||
|
// update positions
|
||||||
|
position_stick_x = getWidth() / 2 - correlated_x;
|
||||||
|
position_stick_y = getHeight() / 2 - correlated_y;
|
||||||
|
|
||||||
|
// Stay active even if we're back in the deadzone because we know the user is actively
|
||||||
|
// giving analog stick input and we don't want to snap back into the deadzone.
|
||||||
|
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||||
|
// them to make precise movements.
|
||||||
|
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||||
|
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||||
|
movement_radius > radius_dead_zone) ?
|
||||||
|
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
|
||||||
|
// trigger move event if state active
|
||||||
|
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
||||||
|
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// save last click state
|
||||||
|
CLICK_STATE lastClickState = click_state;
|
||||||
|
|
||||||
|
// get absolute way for each axis
|
||||||
|
relative_x = -(getWidth() / 2 - event.getX());
|
||||||
|
relative_y = -(getHeight() / 2 - event.getY());
|
||||||
|
|
||||||
|
// get radius and angel of movement from center
|
||||||
|
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||||
|
movement_angle = getAngle(relative_x, relative_y);
|
||||||
|
|
||||||
|
// chop radius if out of outer circle and already pressed
|
||||||
|
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||||
|
// not pressed already, so ignore event from outer circle
|
||||||
|
if (!isPressed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
movement_radius = radius_complete - radius_analog_stick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle event depending on action
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
// down event (touch event)
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
// set to dead zoned, will be corrected in update position if necessary
|
||||||
|
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||||
|
// check for double click
|
||||||
|
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||||
|
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
||||||
|
click_state = CLICK_STATE.DOUBLE;
|
||||||
|
notifyOnDoubleClick();
|
||||||
|
} else {
|
||||||
|
click_state = CLICK_STATE.SINGLE;
|
||||||
|
notifyOnClick();
|
||||||
|
}
|
||||||
|
// reset last click timestamp
|
||||||
|
timeLastClick = System.currentTimeMillis();
|
||||||
|
// set item pressed and update
|
||||||
|
setPressed(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// up event (revoke touch)
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
setPressed(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed()) {
|
||||||
|
// when is pressed calculate new positions (will trigger movement if necessary)
|
||||||
|
updatePosition();
|
||||||
|
} else {
|
||||||
|
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||||
|
notifyOnRevoke();
|
||||||
|
|
||||||
|
// not longer pressed reset analog stick
|
||||||
|
notifyOnMovement(0, 0);
|
||||||
|
}
|
||||||
|
// refresh view
|
||||||
|
invalidate();
|
||||||
|
// accept the touch event
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a digital button on screen element. It is used to get click and double click user input.
|
||||||
|
*/
|
||||||
|
public class DigitalButton extends VirtualControllerElement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener interface to update registered observers.
|
||||||
|
*/
|
||||||
|
public interface DigitalButtonListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onClick event will be fired on button click.
|
||||||
|
*/
|
||||||
|
void onClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onLongClick event will be fired on button long click.
|
||||||
|
*/
|
||||||
|
void onLongClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onRelease event will be fired on button unpress.
|
||||||
|
*/
|
||||||
|
void onRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private class TimerLongClickTimerTask extends TimerTask {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
onLongClickCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
||||||
|
private String text = "";
|
||||||
|
private int icon = -1;
|
||||||
|
private long timerLongClickTimeout = 3000;
|
||||||
|
private Timer timerLongClick = null;
|
||||||
|
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private int layer;
|
||||||
|
private DigitalButton movingButton = null;
|
||||||
|
|
||||||
|
boolean inRange(float x, float y) {
|
||||||
|
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
|
||||||
|
(this.getY() < y && this.getY() + this.getHeight() > y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
|
||||||
|
// check if the movement happened in the same layer
|
||||||
|
if (movingButton.layer != this.layer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save current pressed state
|
||||||
|
boolean wasPressed = isPressed();
|
||||||
|
|
||||||
|
// check if the movement directly happened on the button
|
||||||
|
if ((this.movingButton == null || movingButton == this.movingButton)
|
||||||
|
&& this.inRange(x, y)) {
|
||||||
|
// set button pressed state depending on moving button pressed state
|
||||||
|
if (this.isPressed() != movingButton.isPressed()) {
|
||||||
|
this.setPressed(movingButton.isPressed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if the movement is outside of the range and the movement button
|
||||||
|
// is the saved moving button
|
||||||
|
else if (movingButton == this.movingButton) {
|
||||||
|
this.setPressed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a change occurred
|
||||||
|
if (wasPressed != isPressed()) {
|
||||||
|
if (isPressed()) {
|
||||||
|
// is pressed set moving button and emit click event
|
||||||
|
this.movingButton = movingButton;
|
||||||
|
onClickCallback();
|
||||||
|
} else {
|
||||||
|
// no longer pressed reset moving button and emit release event
|
||||||
|
this.movingButton = null;
|
||||||
|
onReleaseCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkMovementForAllButtons(float x, float y) {
|
||||||
|
for (VirtualControllerElement element : virtualController.getElements()) {
|
||||||
|
if (element != this && element instanceof DigitalButton) {
|
||||||
|
((DigitalButton) element).checkMovement(x, y, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
|
||||||
|
super(controller, context, elementId);
|
||||||
|
this.layer = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setText(String text) {
|
||||||
|
this.text = text;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(int id) {
|
||||||
|
this.icon = id;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setTextSize(getPercent(getWidth(), 30));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||||
|
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
||||||
|
|
||||||
|
if (icon != -1) {
|
||||||
|
Drawable d = getResources().getDrawable(icon);
|
||||||
|
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
||||||
|
d.draw(canvas);
|
||||||
|
} else {
|
||||||
|
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
||||||
|
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onClickCallback() {
|
||||||
|
_DBG("clicked");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
timerLongClick = new Timer();
|
||||||
|
longClickTimerTask = new TimerLongClickTimerTask();
|
||||||
|
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLongClickCallback() {
|
||||||
|
_DBG("long click");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onLongClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onReleaseCallback() {
|
||||||
|
_DBG("released");
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalButtonListener listener : listeners) {
|
||||||
|
listener.onRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We may be called for a release without a prior click
|
||||||
|
if (timerLongClick != null) {
|
||||||
|
timerLongClick.cancel();
|
||||||
|
}
|
||||||
|
if (longClickTimerTask != null) {
|
||||||
|
longClickTimerTask.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// get masked (not specific to a pointer) action
|
||||||
|
float x = getX() + event.getX();
|
||||||
|
float y = getY() + event.getY();
|
||||||
|
int action = event.getActionMasked();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
movingButton = null;
|
||||||
|
setPressed(true);
|
||||||
|
onClickCallback();
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
checkMovementForAllButtons(x, y);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
setPressed(false);
|
||||||
|
onReleaseCallback();
|
||||||
|
|
||||||
|
checkMovementForAllButtons(x, y);
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DigitalPad extends VirtualControllerElement {
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
|
||||||
|
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
||||||
|
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
||||||
|
List<DigitalPadListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final int DPAD_MARGIN = 5;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
public DigitalPad(VirtualController controller, Context context) {
|
||||||
|
super(controller, context, EID_DPAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onElementDraw(Canvas canvas) {
|
||||||
|
// set transparent background
|
||||||
|
canvas.drawColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||||
|
// draw no direction rect
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setColor(getDefaultColor());
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
||||||
|
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw left rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||||
|
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// draw up rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw right rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||||
|
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw down rect
|
||||||
|
paint.setColor(
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawRect(
|
||||||
|
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||||
|
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw left up line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||||
|
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw up right line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||||
|
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw right down line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
||||||
|
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw down left line
|
||||||
|
paint.setColor((
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
||||||
|
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
||||||
|
) ? pressedColor : getDefaultColor()
|
||||||
|
);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
canvas.drawLine(
|
||||||
|
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||||
|
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newDirectionCallback(int direction) {
|
||||||
|
_DBG("direction: " + direction);
|
||||||
|
|
||||||
|
// notify listeners
|
||||||
|
for (DigitalPadListener listener : listeners) {
|
||||||
|
listener.onDirectionChange(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onElementTouchEvent(MotionEvent event) {
|
||||||
|
// get masked (not specific to a pointer) action
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN:
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
direction = 0;
|
||||||
|
|
||||||
|
if (event.getX() < getPercent(getWidth(), 33)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
||||||
|
}
|
||||||
|
if (event.getX() > getPercent(getWidth(), 66)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
||||||
|
}
|
||||||
|
if (event.getY() > getPercent(getHeight(), 66)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
||||||
|
}
|
||||||
|
if (event.getY() < getPercent(getHeight(), 33)) {
|
||||||
|
direction |= DIGITAL_PAD_DIRECTION_UP;
|
||||||
|
}
|
||||||
|
newDirectionCallback(direction);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
direction = 0;
|
||||||
|
newDirectionCallback(direction);
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface DigitalPadListener {
|
||||||
|
void onDirectionChange(int direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class LeftAnalogStick extends AnalogStick {
|
||||||
|
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||||
|
super(controller, context, EID_LS);
|
||||||
|
|
||||||
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
|
@Override
|
||||||
|
public void onMovement(float x, float y) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftStickX = (short) (x * 0x7FFE);
|
||||||
|
inputContext.leftStickY = (short) (y * 0x7FFE);
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoubleClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevoke() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class LeftTrigger extends DigitalButton {
|
||||||
|
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
|
super(controller, EID_LT, layer, context);
|
||||||
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftTrigger = (byte) 0xFF;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.leftTrigger = (byte) 0x00;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
|
||||||
|
public class RightAnalogStick extends AnalogStick {
|
||||||
|
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||||
|
super(controller, context, EID_RS);
|
||||||
|
|
||||||
|
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||||
|
@Override
|
||||||
|
public void onMovement(float x, float y) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightStickX = (short) (x * 0x7FFE);
|
||||||
|
inputContext.rightStickY = (short) (y * 0x7FFE);
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoubleClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRevoke() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class RightTrigger extends DigitalButton {
|
||||||
|
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||||
|
super(controller, EID_RT, layer, context);
|
||||||
|
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightTrigger = (byte) 0xFF;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.rightTrigger = (byte) 0x00;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.input.ControllerHandler;
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
public class VirtualController {
|
||||||
|
public class ControllerInputContext {
|
||||||
|
public short inputMap = 0x0000;
|
||||||
|
public byte leftTrigger = 0x00;
|
||||||
|
public byte rightTrigger = 0x00;
|
||||||
|
public short rightStickX = 0x0000;
|
||||||
|
public short rightStickY = 0x0000;
|
||||||
|
public short leftStickX = 0x0000;
|
||||||
|
public short leftStickY = 0x0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ControllerMode {
|
||||||
|
Active,
|
||||||
|
Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
|
private ControllerHandler controllerHandler;
|
||||||
|
private Context context = null;
|
||||||
|
|
||||||
|
private FrameLayout frame_layout = null;
|
||||||
|
private RelativeLayout relative_layout = null;
|
||||||
|
|
||||||
|
private Timer retransmitTimer;
|
||||||
|
|
||||||
|
ControllerMode currentMode = ControllerMode.Active;
|
||||||
|
ControllerInputContext inputContext = new ControllerInputContext();
|
||||||
|
|
||||||
|
private Button buttonConfigure = null;
|
||||||
|
|
||||||
|
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||||
|
|
||||||
|
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
||||||
|
this.controllerHandler = controllerHandler;
|
||||||
|
this.frame_layout = layout;
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
relative_layout = new RelativeLayout(context);
|
||||||
|
|
||||||
|
frame_layout.addView(relative_layout);
|
||||||
|
|
||||||
|
buttonConfigure = new Button(context);
|
||||||
|
buttonConfigure.setAlpha(0.25f);
|
||||||
|
buttonConfigure.setFocusable(false);
|
||||||
|
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||||
|
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String message;
|
||||||
|
|
||||||
|
if (currentMode == ControllerMode.Configuration) {
|
||||||
|
currentMode = ControllerMode.Active;
|
||||||
|
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||||
|
message = "Exiting configuration mode";
|
||||||
|
} else {
|
||||||
|
currentMode = ControllerMode.Configuration;
|
||||||
|
message = "Entering configuration mode";
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
relative_layout.invalidate();
|
||||||
|
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
relative_layout.removeView(element);
|
||||||
|
}
|
||||||
|
elements.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||||
|
elements.add(element);
|
||||||
|
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||||
|
layoutParams.setMargins(x, y, 0, 0);
|
||||||
|
|
||||||
|
relative_layout.addView(element, layoutParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VirtualControllerElement> getElements() {
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final void _DBG(String text) {
|
||||||
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
|
System.out.println("VirtualController: " + text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshLayout() {
|
||||||
|
relative_layout.removeAllViews();
|
||||||
|
removeElements();
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
|
int buttonSize = (int)(screen.heightPixels*0.06f);
|
||||||
|
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||||
|
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
|
||||||
|
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
|
||||||
|
params.leftMargin = 15;
|
||||||
|
params.topMargin = 15;
|
||||||
|
relative_layout.addView(buttonConfigure, params);
|
||||||
|
|
||||||
|
// Start with the default layout
|
||||||
|
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||||
|
|
||||||
|
// Apply user preferences onto the default layout
|
||||||
|
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerMode getControllerMode() {
|
||||||
|
return currentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControllerInputContext getControllerInputContext() {
|
||||||
|
return inputContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendControllerInputContext() {
|
||||||
|
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||||
|
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||||
|
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||||
|
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||||
|
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||||
|
|
||||||
|
if (controllerHandler != null) {
|
||||||
|
controllerHandler.reportOscState(
|
||||||
|
inputContext.inputMap,
|
||||||
|
inputContext.leftStickX,
|
||||||
|
inputContext.leftStickY,
|
||||||
|
inputContext.rightStickX,
|
||||||
|
inputContext.rightStickY,
|
||||||
|
inputContext.leftTrigger,
|
||||||
|
inputContext.rightTrigger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+328
@@ -0,0 +1,328 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.input.ControllerPacket;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class VirtualControllerConfigurationLoader {
|
||||||
|
public static final String OSC_PREFERENCE = "OSC";
|
||||||
|
|
||||||
|
private static int getPercent(
|
||||||
|
int percent,
|
||||||
|
int total) {
|
||||||
|
return (int) (((float) total / (float) 100) * (float) percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalPad createDigitalPad(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
|
||||||
|
DigitalPad digitalPad = new DigitalPad(controller, context);
|
||||||
|
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
||||||
|
@Override
|
||||||
|
public void onDirectionChange(int direction) {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
|
||||||
|
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||||
|
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
||||||
|
}
|
||||||
|
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
||||||
|
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||||
|
}
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return digitalPad;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createDigitalButton(
|
||||||
|
final int elementId,
|
||||||
|
final int keyShort,
|
||||||
|
final int keyLong,
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
|
||||||
|
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= keyShort;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLongClick() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap |= keyLong;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRelease() {
|
||||||
|
VirtualController.ControllerInputContext inputContext =
|
||||||
|
controller.getControllerInputContext();
|
||||||
|
inputContext.inputMap &= ~keyShort;
|
||||||
|
inputContext.inputMap &= ~keyLong;
|
||||||
|
|
||||||
|
controller.sendControllerInputContext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createLeftTrigger(
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DigitalButton createRightTrigger(
|
||||||
|
final int layer,
|
||||||
|
final String text,
|
||||||
|
final int icon,
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
RightTrigger button = new RightTrigger(controller, layer, context);
|
||||||
|
button.setText(text);
|
||||||
|
button.setIcon(icon);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalogStick createLeftStick(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
return new LeftAnalogStick(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalogStick createRightStick(
|
||||||
|
final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
return new RightAnalogStick(controller, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int BUTTON_BASE_X = 65;
|
||||||
|
private static final int BUTTON_BASE_Y = 5;
|
||||||
|
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
||||||
|
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
||||||
|
|
||||||
|
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||||
|
|
||||||
|
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||||
|
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||||
|
|
||||||
|
if (!config.onlyL3R3)
|
||||||
|
{
|
||||||
|
controller.addElement(createDigitalPad(controller, context),
|
||||||
|
getPercent(5, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(30, screen.widthPixels),
|
||||||
|
getPercent(40, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_A,
|
||||||
|
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_B,
|
||||||
|
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_X,
|
||||||
|
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_Y,
|
||||||
|
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftTrigger(
|
||||||
|
0, "LT", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightTrigger(
|
||||||
|
0, "RT", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_LB,
|
||||||
|
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_RB,
|
||||||
|
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||||
|
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftStick(controller, context),
|
||||||
|
getPercent(5, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightStick(controller, context),
|
||||||
|
getPercent(55, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(50, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_BACK,
|
||||||
|
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||||
|
getPercent(40, screen.widthPixels),
|
||||||
|
getPercent(90, screen.heightPixels),
|
||||||
|
getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_START,
|
||||||
|
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||||
|
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(90, screen.heightPixels),
|
||||||
|
getPercent(10, screen.widthPixels),
|
||||||
|
getPercent(10, screen.heightPixels)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_LSB,
|
||||||
|
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
||||||
|
getPercent(2, screen.widthPixels),
|
||||||
|
getPercent(80, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_RSB,
|
||||||
|
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
||||||
|
getPercent(89, screen.widthPixels),
|
||||||
|
getPercent(80, screen.heightPixels),
|
||||||
|
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||||
|
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveProfile(final VirtualController controller,
|
||||||
|
final Context context) {
|
||||||
|
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
|
||||||
|
|
||||||
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
|
String prefKey = ""+element.elementId;
|
||||||
|
try {
|
||||||
|
prefEditor.putString(prefKey, element.getConfiguration().toString());
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefEditor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||||
|
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
||||||
|
|
||||||
|
for (VirtualControllerElement element : controller.getElements()) {
|
||||||
|
String prefKey = ""+element.elementId;
|
||||||
|
|
||||||
|
String jsonConfig = pref.getString(prefKey, null);
|
||||||
|
if (jsonConfig != null) {
|
||||||
|
try {
|
||||||
|
element.loadConfiguration(new JSONObject(jsonConfig));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// Remove the corrupt element from the preferences
|
||||||
|
pref.edit().remove(prefKey).apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+322
@@ -0,0 +1,322 @@
|
|||||||
|
/**
|
||||||
|
* Created by Karim Mreisi.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.limelight.binding.input.virtual_controller;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public abstract class VirtualControllerElement extends View {
|
||||||
|
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
||||||
|
|
||||||
|
public static final int EID_DPAD = 1;
|
||||||
|
public static final int EID_LT = 2;
|
||||||
|
public static final int EID_RT = 3;
|
||||||
|
public static final int EID_LB = 4;
|
||||||
|
public static final int EID_RB = 5;
|
||||||
|
public static final int EID_A = 6;
|
||||||
|
public static final int EID_B = 7;
|
||||||
|
public static final int EID_X = 8;
|
||||||
|
public static final int EID_Y = 9;
|
||||||
|
public static final int EID_BACK = 10;
|
||||||
|
public static final int EID_START = 11;
|
||||||
|
public static final int EID_LS = 12;
|
||||||
|
public static final int EID_RS = 13;
|
||||||
|
public static final int EID_LSB = 14;
|
||||||
|
public static final int EID_RSB = 15;
|
||||||
|
|
||||||
|
protected VirtualController virtualController;
|
||||||
|
protected final int elementId;
|
||||||
|
|
||||||
|
private final Paint paint = new Paint();
|
||||||
|
|
||||||
|
private int normalColor = 0xF0888888;
|
||||||
|
protected int pressedColor = 0xF00000FF;
|
||||||
|
private int configNormalColor = 0xF0FF0000;
|
||||||
|
private int configSelectedColor = 0xF000FF00;
|
||||||
|
|
||||||
|
protected int startSize_x;
|
||||||
|
protected int startSize_y;
|
||||||
|
|
||||||
|
float position_pressed_x = 0;
|
||||||
|
float position_pressed_y = 0;
|
||||||
|
|
||||||
|
private enum Mode {
|
||||||
|
Normal,
|
||||||
|
Resize,
|
||||||
|
Move
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mode currentMode = Mode.Normal;
|
||||||
|
|
||||||
|
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
this.virtualController = controller;
|
||||||
|
this.elementId = elementId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||||
|
int newPos_x = (int) getX() + x - pressed_x;
|
||||||
|
int newPos_y = (int) getY() + y - pressed_y;
|
||||||
|
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
||||||
|
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
||||||
|
layoutParams.rightMargin = 0;
|
||||||
|
layoutParams.bottomMargin = 0;
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
int newHeight = height + (startSize_y - pressed_y);
|
||||||
|
int newWidth = width + (startSize_x - pressed_x);
|
||||||
|
|
||||||
|
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
||||||
|
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
onElementDraw(canvas);
|
||||||
|
|
||||||
|
if (currentMode != Mode.Normal) {
|
||||||
|
paint.setColor(configSelectedColor);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
|
||||||
|
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||||
|
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
||||||
|
paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
protected void actionShowNormalColorChooser() {
|
||||||
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(AmbilWarnaDialog dialog)
|
||||||
|
{}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
|
normalColor = color;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
colorDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionShowPressedColorChooser() {
|
||||||
|
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(AmbilWarnaDialog dialog) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||||
|
pressedColor = color;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
colorDialog.show();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected void actionEnableMove() {
|
||||||
|
currentMode = Mode.Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionEnableResize() {
|
||||||
|
currentMode = Mode.Resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void actionCancel() {
|
||||||
|
currentMode = Mode.Normal;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDefaultColor() {
|
||||||
|
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||||
|
configNormalColor : normalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getDefaultStrokeWidth() {
|
||||||
|
DisplayMetrics screen = getResources().getDisplayMetrics();
|
||||||
|
return (int)(screen.heightPixels*0.004f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void showConfigurationDialog() {
|
||||||
|
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||||
|
|
||||||
|
alertBuilder.setTitle("Configuration");
|
||||||
|
|
||||||
|
CharSequence functions[] = new CharSequence[]{
|
||||||
|
"Move",
|
||||||
|
"Resize",
|
||||||
|
/*election
|
||||||
|
"Set n
|
||||||
|
Disable color sormal color",
|
||||||
|
"Set pressed color",
|
||||||
|
*/
|
||||||
|
"Cancel"
|
||||||
|
};
|
||||||
|
|
||||||
|
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which) {
|
||||||
|
case 0: { // move
|
||||||
|
actionEnableMove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: { // resize
|
||||||
|
actionEnableResize();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
case 2: { // set default color
|
||||||
|
actionShowNormalColorChooser();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: { // set pressed color
|
||||||
|
actionShowPressedColorChooser();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
default: { // cancel
|
||||||
|
actionCancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AlertDialog alert = alertBuilder.create();
|
||||||
|
// show menu
|
||||||
|
alert.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
||||||
|
return onElementTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
position_pressed_x = event.getX();
|
||||||
|
position_pressed_y = event.getY();
|
||||||
|
startSize_x = getWidth();
|
||||||
|
startSize_y = getHeight();
|
||||||
|
|
||||||
|
actionEnableMove();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
switch (currentMode) {
|
||||||
|
case Move: {
|
||||||
|
moveElement(
|
||||||
|
(int) position_pressed_x,
|
||||||
|
(int) position_pressed_y,
|
||||||
|
(int) event.getX(),
|
||||||
|
(int) event.getY());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Resize: {
|
||||||
|
resizeElement(
|
||||||
|
(int) position_pressed_x,
|
||||||
|
(int) position_pressed_y,
|
||||||
|
(int) event.getX(),
|
||||||
|
(int) event.getY());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Normal: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_POINTER_UP: {
|
||||||
|
actionCancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected void onElementDraw(Canvas canvas);
|
||||||
|
|
||||||
|
abstract public boolean onElementTouchEvent(MotionEvent event);
|
||||||
|
|
||||||
|
protected static final void _DBG(String text) {
|
||||||
|
if (_PRINT_DEBUG_INFORMATION) {
|
||||||
|
System.out.println(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColors(int normalColor, int pressedColor) {
|
||||||
|
this.normalColor = normalColor;
|
||||||
|
this.pressedColor = pressedColor;
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final float getPercent(float value, float percent) {
|
||||||
|
return value / 100 * percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final int getCorrectWidth() {
|
||||||
|
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public JSONObject getConfiguration() throws JSONException {
|
||||||
|
JSONObject configuration = new JSONObject();
|
||||||
|
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
configuration.put("LEFT", layoutParams.leftMargin);
|
||||||
|
configuration.put("TOP", layoutParams.topMargin);
|
||||||
|
configuration.put("WIDTH", layoutParams.width);
|
||||||
|
configuration.put("HEIGHT", layoutParams.height);
|
||||||
|
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
||||||
|
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
layoutParams.leftMargin = configuration.getInt("LEFT");
|
||||||
|
layoutParams.topMargin = configuration.getInt("TOP");
|
||||||
|
layoutParams.width = configuration.getInt("WIDTH");
|
||||||
|
layoutParams.height = configuration.getInt("HEIGHT");
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
public interface CrashListener {
|
||||||
|
void notifyCrash(Exception e);
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
|
||||||
|
|
||||||
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
|
|
||||||
public abstract boolean isHevcSupported();
|
|
||||||
public abstract boolean isAvcSupported();
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,10 @@ import java.util.Collections;
|
|||||||
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.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ConfigurationInfo;
|
import android.content.pm.ConfigurationInfo;
|
||||||
@@ -27,14 +28,21 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
private static final List<String> blacklistedDecoderPrefixes;
|
private static final List<String> blacklistedDecoderPrefixes;
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
||||||
|
private static final List<String> deprioritizedHevcDecoders;
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
private static final List<String> baselineProfileHackPrefixes;
|
||||||
private static final List<String> directSubmitPrefixes;
|
private static final List<String> directSubmitPrefixes;
|
||||||
private static final List<String> constrainedHighProfilePrefixes;
|
private static final List<String> constrainedHighProfilePrefixes;
|
||||||
private static final List<String> whitelistedHevcDecoders;
|
private static final List<String> whitelistedHevcDecoders;
|
||||||
|
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||||
|
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||||
|
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
||||||
|
|
||||||
|
private static boolean isLowEndSnapdragon = false;
|
||||||
|
private static boolean initialized = false;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
directSubmitPrefixes = new LinkedList<String>();
|
directSubmitPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// These decoders have low enough input buffer latency that they
|
// These decoders have low enough input buffer latency that they
|
||||||
// can be directly invoked from the receive thread
|
// can be directly invoked from the receive thread
|
||||||
@@ -45,68 +53,238 @@ public class MediaCodecHelper {
|
|||||||
directSubmitPrefixes.add("omx.brcm");
|
directSubmitPrefixes.add("omx.brcm");
|
||||||
directSubmitPrefixes.add("omx.TI");
|
directSubmitPrefixes.add("omx.TI");
|
||||||
directSubmitPrefixes.add("omx.arc");
|
directSubmitPrefixes.add("omx.arc");
|
||||||
|
directSubmitPrefixes.add("omx.nvidia");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
refFrameInvalidationAvcPrefixes = new LinkedList<>();
|
||||||
|
refFrameInvalidationHevcPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
|
// Qualcomm and NVIDIA may be added at runtime
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
preferredDecoders = new LinkedList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
// Software decoders that don't support H264 high profile
|
// Blacklist software decoders that don't support H264 high profile,
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
// but exclude the official AOSP and CrOS emulator from this restriction.
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
||||||
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never use ffmpeg decoders since they're software decoders
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
||||||
|
|
||||||
|
// Force these decoders disabled because:
|
||||||
|
// 1) They are software decoders, so the performance is terrible
|
||||||
|
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
|
||||||
|
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
// If a decoder qualifies for reference frame invalidation,
|
||||||
|
// these entries will be ignored for those decoders.
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||||
|
|
||||||
baselineProfileHackPrefixes = new LinkedList<String>();
|
baselineProfileHackPrefixes = new LinkedList<>();
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
// if adaptive playback was enabled so let's avoid it to be safe.
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
||||||
|
// on some Android TV devices with H.265 only.
|
||||||
|
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<String>();
|
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
constrainedHighProfilePrefixes.add("omx.intel");
|
||||||
|
|
||||||
whitelistedHevcDecoders = new LinkedList<>();
|
|
||||||
whitelistedHevcDecoders.add("omx.exynos");
|
|
||||||
// whitelistedHevcDecoders.add("omx.nvidia"); TODO: This needs a similar fixup to the Tegra 3
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
|
||||||
whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
whitelistedHevcDecoders.add("omx.rk");
|
|
||||||
// omx.qcom added conditionally during initialization
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initializeWithContext(Context context) {
|
static {
|
||||||
|
whitelistedHevcDecoders = new LinkedList<>();
|
||||||
|
|
||||||
|
// Allow software HEVC decoding in the official AOSP emulator
|
||||||
|
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
|
||||||
|
whitelistedHevcDecoders.add("omx.google");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exynos seems to be the only HEVC decoder that works reliably
|
||||||
|
whitelistedHevcDecoders.add("omx.exynos");
|
||||||
|
|
||||||
|
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
|
||||||
|
// For some reason, other X1 implementations require bitstream fixups.
|
||||||
|
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
|
||||||
|
whitelistedHevcDecoders.add("omx.nvidia");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||||
|
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
|
||||||
|
// to actually be tested.
|
||||||
|
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||||
|
whitelistedHevcDecoders.add("omx.mtk");
|
||||||
|
whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
|
}
|
||||||
|
|
||||||
|
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||||
|
// their AVC decoders), but haven't been tested enough
|
||||||
|
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||||
|
//whitelistedHevcDecoders.add("omx.rk");
|
||||||
|
|
||||||
|
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
||||||
|
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
deprioritizedHevcDecoders = new LinkedList<>();
|
||||||
|
|
||||||
|
// These are decoders that work but aren't used by default for various reasons.
|
||||||
|
|
||||||
|
// Qualcomm is currently the only decoders in this group.
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
||||||
|
|
||||||
|
// We see a bunch of crashes on MediaTek Android TVs running
|
||||||
|
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
||||||
|
// these devices and hope they fix it in Pie.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPowerVR(String glRenderer) {
|
||||||
|
return glRenderer.toLowerCase().contains("powervr");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getAdrenoVersionString(String glRenderer) {
|
||||||
|
glRenderer = glRenderer.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (!glRenderer.contains("adreno")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
||||||
|
|
||||||
|
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String modelNumber = matcher.group(2);
|
||||||
|
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
||||||
|
return modelNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
||||||
|
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||||
|
if (modelNumber == null) {
|
||||||
|
// Not an Adreno GPU
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
||||||
|
return modelNumber.charAt(1) == '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a workaround for some broken devices that report
|
||||||
|
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
||||||
|
// An example of such a device is the Huawei Honor 5x with the
|
||||||
|
// Snapdragon 616 SoC (Adreno 405).
|
||||||
|
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
||||||
|
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||||
|
if (modelNumber == null) {
|
||||||
|
// Not an Adreno GPU
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapdragon 4xx and higher support GLES 3.1
|
||||||
|
return modelNumber.charAt(0) >= '4';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initialize(Context context, String glRenderer) {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ActivityManager activityManager =
|
ActivityManager activityManager =
|
||||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
||||||
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
||||||
|
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||||
|
|
||||||
|
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
||||||
|
|
||||||
|
// Tegra K1 and later can do reference frame invalidation properly
|
||||||
|
if (configInfo.reqGlEsVersion >= 0x30000) {
|
||||||
|
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
||||||
|
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
||||||
|
|
||||||
|
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
||||||
|
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
||||||
|
|
||||||
|
// Prior to M, we were tricking the decoder into using baseline profile, which
|
||||||
|
// won't support RFI properly.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
||||||
|
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
||||||
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
||||||
// 3xx - bad
|
// 3xx - bad
|
||||||
// 4xx - good
|
// 4xx - good
|
||||||
//
|
//
|
||||||
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
||||||
// approximation by checking the GLES level (<= 3.0 is bad).
|
// (see comment on isGLES31SnapdragonRenderer).
|
||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
//
|
||||||
if (configInfo.reqGlEsVersion > 0x30000) {
|
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
||||||
LimeLog.info("Added omx.qcom to supported decoders based on GLES 3.1+ support");
|
// We prefer reference frame invalidation support (which is only doable on AVC on
|
||||||
whitelistedHevcDecoders.add("omx.qcom");
|
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||||
|
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
||||||
|
// HEVC anyway.
|
||||||
|
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
|
||||||
|
deprioritizedHevcDecoders.add("omx.qcom");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
|
if (!initialized) {
|
||||||
|
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
||||||
|
}
|
||||||
|
|
||||||
for (String badPrefix : decoderList) {
|
for (String badPrefix : decoderList) {
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
if (decoderName.length() >= badPrefix.length()) {
|
||||||
String prefix = decoderName.substring(0, badPrefix.length());
|
String prefix = decoderName.substring(0, badPrefix.length());
|
||||||
@@ -122,20 +300,15 @@ public class MediaCodecHelper {
|
|||||||
public static long getMonotonicMillis() {
|
public static long getMonotonicMillis() {
|
||||||
return System.nanoTime() / 1000000L;
|
return System.nanoTime() / 1000000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
|
|
||||||
/*
|
|
||||||
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
|
||||||
so we'll keep it off for now, since we don't know whether other devices also do the same
|
|
||||||
|
|
||||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
||||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
// Possibly enable adaptive playback on KitKat and above
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
|
||||||
|
LimeLog.info("Decoder blacklisted for adaptive playback");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||||
@@ -147,7 +320,7 @@ public class MediaCodecHelper {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Tolerate buggy codecs
|
// Tolerate buggy codecs
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -168,7 +341,30 @@ public class MediaCodecHelper {
|
|||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
|
||||||
|
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
||||||
|
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
||||||
|
if (videoHeight > 720 && isLowEndSnapdragon) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This device seems to crash constantly at 720p, so try disabling
|
||||||
|
// RFI to see if we can get that under control.
|
||||||
|
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
||||||
|
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
||||||
// TODO: Shield Tablet K1/LTE?
|
// TODO: Shield Tablet K1/LTE?
|
||||||
//
|
//
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||||
@@ -190,13 +386,32 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Software decoders are terrible and we never want to use them.
|
||||||
|
// We want to catch decoders like:
|
||||||
|
// OMX.qcom.video.decoder.hevcswvdec
|
||||||
|
// OMX.SEC.hevc.sw.dec
|
||||||
|
//
|
||||||
|
if (decoderName.contains("sw")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some devices have HEVC decoders that we prefer not to use
|
||||||
|
// typically because it can't support reference frame invalidation.
|
||||||
|
// However, we will use it for HDR and for streaming over mobile networks
|
||||||
|
// since it works fine otherwise.
|
||||||
|
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
||||||
|
LimeLog.info("Selected deprioritized decoder");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
@@ -237,6 +452,10 @@ public class MediaCodecHelper {
|
|||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
// because we want to evaluate the decoders in our list's order
|
// because we want to evaluate the decoders in our list's order
|
||||||
// rather than MediaCodecList's order
|
// rather than MediaCodecList's order
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
||||||
|
}
|
||||||
|
|
||||||
for (String preferredDecoder : preferredDecoders) {
|
for (String preferredDecoder : preferredDecoders) {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
@@ -354,7 +573,7 @@ public class MediaCodecHelper {
|
|||||||
break;
|
break;
|
||||||
cpuInfo.append((char)ch);
|
cpuInfo.append((char)ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cpuInfo.toString();
|
return cpuInfo.toString();
|
||||||
} finally {
|
} finally {
|
||||||
br.close();
|
br.close();
|
||||||
|
|||||||
@@ -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,13 +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 = "computers2.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 LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
||||||
|
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
||||||
|
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||||
|
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
@@ -36,79 +39,101 @@ 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) {
|
||||||
|
// Add cert column to the table if not present
|
||||||
|
try {
|
||||||
|
computerDb.execSQL(String.format((Locale)null,
|
||||||
|
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
||||||
|
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
||||||
|
} catch (SQLiteException e) {}
|
||||||
|
|
||||||
|
|
||||||
// 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, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
||||||
COMPUTER_TABLE_NAME,
|
COMPUTER_TABLE_NAME,
|
||||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
||||||
|
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||||
|
|
||||||
|
// Move all computers from the old DB (if any) to the new one
|
||||||
|
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||||
|
for (ComputerDetails computer : oldComputers) {
|
||||||
|
updateComputer(computer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
public void deleteComputer(String name) {
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
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_ADDRESS_COLUMN_NAME, details.localAddress);
|
||||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
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 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);
|
||||||
|
|
||||||
|
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 List<ComputerDetails> getAllComputers() {
|
public List<ComputerDetails> getAllComputers() {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
ComputerDetails details = new ComputerDetails();
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
details.name = c.getString(0);
|
// If a critical field is corrupt or missing, skip the database entry
|
||||||
|
if (details.uuid == null) {
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
computerList.add(details);
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,49 +142,19 @@ public class ComputerDatabaseManager {
|
|||||||
return computerList;
|
return computerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
public ComputerDetails getComputerByUUID(String uuid) {
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
if (!c.moveToFirst()) {
|
if (!c.moveToFirst()) {
|
||||||
// No matching computer
|
// No matching computer
|
||||||
c.close();
|
c.close();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.name = c.getString(0);
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
// If a critical field is corrupt or missing, delete the database entry
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
if (details.uuid == null) {
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
deleteComputer(details.name);
|
deleteComputer(details.name);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
public interface ComputerManagerListener {
|
||||||
public void notifyComputerUpdated(ComputerDetails details);
|
void notifyComputerUpdated(ComputerDetails details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,25 @@ 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.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
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;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -35,8 +37,11 @@ public class ComputerManagerService extends Service {
|
|||||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
private static final int FAST_POLL_TIMEOUT = 500;
|
private static final int FAST_POLL_TIMEOUT = 1000;
|
||||||
private static final int OFFLINE_POLL_TRIES = 5;
|
private static final int OFFLINE_POLL_TRIES = 5;
|
||||||
|
private static final int INITIAL_POLL_TRIES = 2;
|
||||||
|
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||||
|
private static final int POLL_DATA_TTL_MS = 30000;
|
||||||
|
|
||||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
@@ -44,7 +49,7 @@ public class ComputerManagerService extends Service {
|
|||||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||||
|
|
||||||
private IdentityManager idManager;
|
private IdentityManager idManager;
|
||||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
|
||||||
private ComputerManagerListener listener = null;
|
private ComputerManagerListener listener = null;
|
||||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||||
private boolean pollingActive = false;
|
private boolean pollingActive = false;
|
||||||
@@ -75,19 +80,21 @@ public class ComputerManagerService extends Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final int pollTriesBeforeOffline = details.state == ComputerDetails.State.UNKNOWN ?
|
||||||
|
INITIAL_POLL_TRIES : OFFLINE_POLL_TRIES;
|
||||||
|
|
||||||
activePolls.incrementAndGet();
|
activePolls.incrementAndGet();
|
||||||
|
|
||||||
// Poll the machine
|
// Poll the machine
|
||||||
try {
|
try {
|
||||||
if (!pollComputer(details)) {
|
if (!pollComputer(details)) {
|
||||||
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
if (!newPc && offlineCount < pollTriesBeforeOffline) {
|
||||||
// Return without calling the listener
|
// Return without calling the listener
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
@@ -98,17 +105,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
|
||||||
@@ -126,7 +143,7 @@ public class ComputerManagerService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
int offlineCount = 0;
|
int offlineCount = 0;
|
||||||
while (!isInterrupted() && pollingActive) {
|
while (!isInterrupted() && pollingActive && tuple.thread == this) {
|
||||||
try {
|
try {
|
||||||
// Only allow one request to the machine at a time
|
// Only allow one request to the machine at a time
|
||||||
synchronized (tuple.networkLock) {
|
synchronized (tuple.networkLock) {
|
||||||
@@ -135,6 +152,7 @@ public class ComputerManagerService extends Service {
|
|||||||
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
||||||
offlineCount++;
|
offlineCount++;
|
||||||
} else {
|
} else {
|
||||||
|
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
|
||||||
offlineCount = 0;
|
offlineCount = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +165,7 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
t.setName("Polling thread for " + tuple.computer.name);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +182,17 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
synchronized (pollingTuples) {
|
synchronized (pollingTuples) {
|
||||||
for (PollingTuple tuple : pollingTuples) {
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
// Enforce the poll data TTL
|
||||||
|
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||||
|
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||||
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report this computer initially
|
||||||
|
listener.notifyComputerUpdated(tuple.computer);
|
||||||
|
|
||||||
// This polling thread might already be there
|
// This polling thread might already be there
|
||||||
if (tuple.thread == null) {
|
if (tuple.thread == null) {
|
||||||
// Report this computer initially
|
|
||||||
listener.notifyComputerUpdated(tuple.computer);
|
|
||||||
|
|
||||||
tuple.thread = createPollingThread(tuple);
|
tuple.thread = createPollingThread(tuple);
|
||||||
tuple.thread.start();
|
tuple.thread.start();
|
||||||
}
|
}
|
||||||
@@ -196,8 +220,8 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(InetAddress addr) {
|
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeComputer(String name) {
|
public void removeComputer(String name) {
|
||||||
@@ -217,7 +241,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)) {
|
||||||
@@ -228,12 +252,28 @@ public class ComputerManagerService extends Service {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void invalidateStateForComputer(String uuid) {
|
||||||
|
synchronized (pollingTuples) {
|
||||||
|
for (PollingTuple tuple : pollingTuples) {
|
||||||
|
if (uuid.equals(tuple.computer.uuid)) {
|
||||||
|
// We need the network lock to prevent a concurrent poll
|
||||||
|
// from wiping this change out
|
||||||
|
synchronized (tuple.networkLock) {
|
||||||
|
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onUnbind(Intent intent) {
|
public boolean onUnbind(Intent intent) {
|
||||||
// Stop mDNS autodiscovery
|
if (discoveryBinder != null) {
|
||||||
discoveryBinder.stopDiscovery();
|
// Stop mDNS autodiscovery
|
||||||
|
discoveryBinder.stopDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
pollingActive = false;
|
pollingActive = false;
|
||||||
@@ -258,7 +298,7 @@ public class ComputerManagerService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
// Kick off a serverinfo poll on this machine
|
// Kick off a serverinfo poll on this machine
|
||||||
addComputerBlocking(computer.getAddress());
|
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -279,10 +319,8 @@ public class ComputerManagerService extends Service {
|
|||||||
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)) {
|
||||||
// Update details anyway in case this machine has been re-added by IP
|
// Update the saved computer with potentially new details
|
||||||
// after not being reachable by our existing information
|
tuple.computer.update(details);
|
||||||
tuple.computer.localIp = details.localIp;
|
|
||||||
tuple.computer.remoteIp = details.remoteIp;
|
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -307,15 +345,43 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addComputerBlocking(InetAddress addr) {
|
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||||
// Setup a placeholder
|
// Setup a placeholder
|
||||||
ComputerDetails fakeDetails = new ComputerDetails();
|
ComputerDetails fakeDetails = new ComputerDetails();
|
||||||
fakeDetails.localIp = addr;
|
|
||||||
fakeDetails.remoteIp = addr;
|
if (manuallyAdded) {
|
||||||
|
// Add PC UI
|
||||||
|
fakeDetails.manualAddress = addr;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// mDNS
|
||||||
|
fakeDetails.localAddress = addr;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||||
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
@@ -329,6 +395,10 @@ public class ComputerManagerService extends Service {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (!manuallyAdded) {
|
||||||
|
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,6 +418,7 @@ public class ComputerManagerService extends Service {
|
|||||||
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();
|
||||||
|
tuple.thread = null;
|
||||||
}
|
}
|
||||||
pollingTuples.remove(tuple);
|
pollingTuples.remove(tuple);
|
||||||
break;
|
break;
|
||||||
@@ -373,38 +444,51 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
||||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||||
if (!fastPollIp(ipAddr)) {
|
if (!fastPollIp(address)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http = new NvHTTP(ipAddr, 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(InetAddress addr) {
|
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(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
s.close();
|
s.close();
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -412,11 +496,11 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
private void startFastPollThread(final String address, final boolean[] info) {
|
||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean pollRes = fastPollIp(addr);
|
boolean pollRes = fastPollIp(address);
|
||||||
|
|
||||||
synchronized (info) {
|
synchronized (info) {
|
||||||
info[0] = true; // Done
|
info[0] = true; // Done
|
||||||
@@ -426,16 +510,18 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
t.setName("Fast Poll - "+addr.getHostAddress());
|
t.setName("Fast Poll - "+address);
|
||||||
t.start();
|
t.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) 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];
|
||||||
|
|
||||||
startFastPollThread(local, localInfo);
|
startFastPollThread(localAddress, localInfo);
|
||||||
startFastPollThread(remote, remoteInfo);
|
startFastPollThread(remoteAddress, remoteInfo);
|
||||||
|
startFastPollThread(manualAddress, manualInfo);
|
||||||
|
|
||||||
// Check local first
|
// Check local first
|
||||||
synchronized (localInfo) {
|
synchronized (localInfo) {
|
||||||
@@ -444,125 +530,78 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally, remote
|
||||||
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;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
|
||||||
ComputerDetails polledDetails;
|
|
||||||
ComputerDetails.Reachability reachability;
|
|
||||||
|
|
||||||
// If the local address is routable across the Internet,
|
|
||||||
// always consider this PC remote to be conservative
|
|
||||||
if (details.localIp.equals(details.remoteIp)) {
|
|
||||||
reachability = ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
|
||||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
|
||||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
|
||||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
|
||||||
|
|
||||||
// If no connection could be established to either IP address, there's nothing we can do
|
|
||||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
|
||||||
|
|
||||||
if (localFirst) {
|
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
|
||||||
}
|
|
||||||
|
|
||||||
InetAddress reachableAddr = null;
|
|
||||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
|
||||||
// Failed, so let's try the fallback
|
|
||||||
if (!localFirst) {
|
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
polledDetails = tryPollIp(details, details.remoteIp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polledDetails != null) {
|
|
||||||
// The fallback poll worked
|
|
||||||
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (polledDetails != null) {
|
|
||||||
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reachableAddr == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
|
||||||
}
|
|
||||||
else if (polledDetails.localIp.equals(reachableAddr)) {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReachabilityTuple(polledDetails, reachableAddr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
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 +")");
|
||||||
|
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
|
||||||
|
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.localIp = initialReachTuple.reachableAddress;
|
uniqueAddresses.add(details.localAddress);
|
||||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
uniqueAddresses.add(details.remoteAddress);
|
||||||
}
|
uniqueAddresses.add(details.manualAddress);
|
||||||
else {
|
for (String addr : uniqueAddresses) {
|
||||||
// We got it on one of the returned addresses; replace the original reach tuple
|
if (addr == null || addr.equals(candidateAddress)) {
|
||||||
// with the new one
|
continue;
|
||||||
initialReachTuple = confirmationReachTuple;
|
}
|
||||||
|
polledDetails = tryPollIp(details, addr);
|
||||||
|
if (polledDetails != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the old MAC address
|
if (polledDetails != null) {
|
||||||
String savedMacAddress = details.macAddress;
|
details.update(polledDetails);
|
||||||
|
return true;
|
||||||
// If we got here, it's reachable
|
}
|
||||||
details.update(initialReachTuple.computer);
|
else {
|
||||||
|
return false;
|
||||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
|
||||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
|
||||||
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
|
||||||
details.macAddress = savedMacAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -661,11 +700,11 @@ public class ComputerManagerService extends Service {
|
|||||||
thread = new Thread() {
|
thread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
int emptyAppListResponses = 0;
|
||||||
do {
|
do {
|
||||||
InetAddress selectedAddr;
|
// Can't poll if it's not online or paired
|
||||||
|
if (computer.state != ComputerDetails.State.ONLINE ||
|
||||||
// Can't poll if it's not online
|
computer.pairState != PairingManager.PairState.PAIRED) {
|
||||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
@@ -677,19 +716,12 @@ public class ComputerManagerService extends Service {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
selectedAddr = computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
selectedAddr = computer.remoteIp;
|
|
||||||
}
|
|
||||||
|
|
||||||
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
|
||||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
|
||||||
|
|
||||||
PollingTuple tuple = getPollingTuple(computer);
|
PollingTuple tuple = getPollingTuple(computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||||
|
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
String appList;
|
String appList;
|
||||||
if (tuple != null) {
|
if (tuple != null) {
|
||||||
// If we're polling this machine too, grab the network lock
|
// If we're polling this machine too, grab the network lock
|
||||||
@@ -705,11 +737,19 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
|
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||||
|
|
||||||
|
// The app list might actually be empty, so if we get an empty response a few times
|
||||||
|
// in a row, we'll go ahead and believe it.
|
||||||
|
emptyAppListResponses++;
|
||||||
|
}
|
||||||
|
if (!appList.isEmpty() &&
|
||||||
|
(!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();
|
||||||
@@ -721,6 +761,11 @@ public class ComputerManagerService extends Service {
|
|||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset empty count if it wasn't empty this time
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
emptyAppListResponses = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the computer
|
// Update the computer
|
||||||
computer.rawAppList = appList;
|
computer.rawAppList = appList;
|
||||||
receivedAppList = true;
|
receivedAppList = true;
|
||||||
@@ -731,8 +776,8 @@ public class ComputerManagerService extends Service {
|
|||||||
listener.notifyComputerUpdated(computer);
|
listener.notifyComputerUpdated(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if (appList.isEmpty()) {
|
||||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -742,6 +787,7 @@ public class ComputerManagerService extends Service {
|
|||||||
} while (waitPollingDelay());
|
} while (waitPollingDelay());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
thread.setName("App list polling thread for " + computer.name);
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,6 +807,7 @@ class PollingTuple {
|
|||||||
public Thread thread;
|
public Thread thread;
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
public final Object networkLock;
|
public final Object networkLock;
|
||||||
|
public long lastSuccessfulPollMs;
|
||||||
|
|
||||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
@@ -770,10 +817,10 @@ class PollingTuple {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReachabilityTuple {
|
class ReachabilityTuple {
|
||||||
public final InetAddress reachableAddress;
|
public final String reachableAddress;
|
||||||
public final ComputerDetails computer;
|
public final ComputerDetails computer;
|
||||||
|
|
||||||
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.reachableAddress = reachableAddress;
|
this.reachableAddress = reachableAddress;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ public class DiscoveryService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||||
multicastLock.setReferenceCounted(false);
|
multicastLock.setReferenceCounted(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
@@ -14,8 +14,6 @@ import com.limelight.grid.assets.MemoryAssetLoader;
|
|||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import java.security.KeyManagementException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
private final CachedAppAssetLoader loader;
|
private final CachedAppAssetLoader loader;
|
||||||
|
|
||||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
||||||
|
|
||||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||||
int dp;
|
int dp;
|
||||||
@@ -47,15 +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));
|
||||||
BitmapFactory.decodeResource(activity.getResources(),
|
|
||||||
R.drawable.image_loading, options));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
public void cancelQueuedOperations() {
|
||||||
@@ -68,7 +61,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||||
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
|
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,9 +79,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
itemList.remove(app);
|
itemList.remove(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
@Override
|
||||||
|
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
||||||
// Let the cached asset loader handle it
|
// Let the cached asset loader handle it
|
||||||
loader.populateImageView(obj.app, imgView);
|
loader.populateImageView(obj.app, imgView, prgView);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +97,9 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||||
if (obj.app.getIsRunning()) {
|
if (obj.isRunning) {
|
||||||
// Show the play button overlay
|
// Show the play button overlay
|
||||||
overlayView.setImageResource(R.drawable.play);
|
overlayView.setImageResource(R.drawable.ic_play);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.BaseAdapter;
|
import android.widget.BaseAdapter;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
@@ -14,15 +15,13 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
protected final int defaultImageRes;
|
|
||||||
protected final int layoutId;
|
protected final int layoutId;
|
||||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
protected final ArrayList<T> itemList = new ArrayList<>();
|
||||||
protected final LayoutInflater inflater;
|
protected final LayoutInflater inflater;
|
||||||
|
|
||||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
public GenericGridAdapter(Context context, int layoutId) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.layoutId = layoutId;
|
this.layoutId = layoutId;
|
||||||
this.defaultImageRes = defaultImageRes;
|
|
||||||
|
|
||||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
}
|
}
|
||||||
@@ -46,7 +45,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract boolean populateImageView(ImageView imgView, T obj);
|
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
|
||||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||||
|
|
||||||
@@ -56,13 +55,14 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
ImageView imgView = convertView.findViewById(R.id.grid_image);
|
||||||
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
||||||
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||||
|
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
||||||
|
|
||||||
if (imgView != null) {
|
if (imgView != null) {
|
||||||
if (!populateImageView(imgView, itemList.get(i))) {
|
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||||
imgView.setImageResource(defaultImageRes);
|
imgView.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!populateTextView(txtView, itemList.get(i))) {
|
if (!populateTextView(txtView, itemList.get(i))) {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
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;
|
||||||
@@ -14,7 +17,7 @@ import java.util.Comparator;
|
|||||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
||||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
|
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addComputer(PcView.ComputerObject computer) {
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
@@ -26,7 +29,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
||||||
return lhs.details.name.compareTo(rhs.details.name);
|
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -36,21 +39,28 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
||||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
imgView.setAlpha(1.0f);
|
imgView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
imgView.setAlpha(0.4f);
|
imgView.setAlpha(0.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return false to use the default drawable
|
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
|
||||||
return false;
|
prgView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
imgView.setImageResource(R.drawable.ic_computer);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||||
txtView.setAlpha(1.0f);
|
txtView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -63,13 +73,19 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
||||||
// Still refreshing this PC so display the overlay
|
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
||||||
overlayView.setImageResource(R.drawable.image_loading);
|
overlayView.setAlpha(0.4f);
|
||||||
|
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 true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No overlay
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import android.graphics.Bitmap;
|
|||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
@@ -53,13 +55,13 @@ public class CachedAppAssetLoader {
|
|||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
DiskAssetLoader diskLoader) {
|
||||||
this.computer = computer;
|
this.computer = computer;
|
||||||
this.scalingDivider = scalingDivider;
|
this.scalingDivider = scalingDivider;
|
||||||
this.networkLoader = networkLoader;
|
this.networkLoader = networkLoader;
|
||||||
this.memoryLoader = memoryLoader;
|
this.memoryLoader = memoryLoader;
|
||||||
this.diskLoader = diskLoader;
|
this.diskLoader = diskLoader;
|
||||||
this.placeholderBitmap = placeholderBitmap;
|
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelBackgroundLoads() {
|
public void cancelBackgroundLoads() {
|
||||||
@@ -130,12 +132,14 @@ public class CachedAppAssetLoader {
|
|||||||
|
|
||||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||||
private final WeakReference<ImageView> imageViewRef;
|
private final WeakReference<ImageView> imageViewRef;
|
||||||
|
private final WeakReference<ProgressBar> progressViewRef;
|
||||||
private final boolean diskOnly;
|
private final boolean diskOnly;
|
||||||
|
|
||||||
private LoaderTuple tuple;
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
||||||
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
this.imageViewRef = new WeakReference<>(imageView);
|
||||||
|
this.progressViewRef = new WeakReference<>(prgView);
|
||||||
this.diskOnly = diskOnly;
|
this.diskOnly = diskOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +147,8 @@ public class CachedAppAssetLoader {
|
|||||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||||
tuple = params[0];
|
tuple = params[0];
|
||||||
|
|
||||||
// Check whether it has been cancelled or the image view is gone
|
// Check whether it has been cancelled or the views are gone
|
||||||
if (isCancelled() || imageViewRef.get() == null) {
|
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,11 +181,17 @@ public class CachedAppAssetLoader {
|
|||||||
|
|
||||||
// If the current loader task for this view isn't us, do nothing
|
// If the current loader task for this view isn't us, do nothing
|
||||||
final ImageView imageView = imageViewRef.get();
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
final ProgressBar prgView = progressViewRef.get();
|
||||||
if (getLoaderTask(imageView) == this) {
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Now display the progress bar since we have to hit the network
|
||||||
|
if (prgView != null) {
|
||||||
|
prgView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
// Set off another loader task on the network executor
|
// Set off another loader task on the network executor
|
||||||
LoaderTask task = new LoaderTask(imageView, false);
|
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||||
imageView.setAlpha(1.0f);
|
imageView.setVisibility(View.VISIBLE);
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
}
|
}
|
||||||
@@ -195,14 +205,20 @@ public class CachedAppAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
final ProgressBar prgView = progressViewRef.get();
|
||||||
if (getLoaderTask(imageView) == this) {
|
if (getLoaderTask(imageView) == this) {
|
||||||
// Set the bitmap
|
// Set the bitmap
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
imageView.setImageBitmap(bitmap);
|
imageView.setImageBitmap(bitmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the progress bar
|
||||||
|
if (prgView != null) {
|
||||||
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
// Show the view
|
// Show the view
|
||||||
imageView.setAlpha(1.0f);
|
imageView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +229,7 @@ public class CachedAppAssetLoader {
|
|||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
LoaderTask loaderTask) {
|
LoaderTask loaderTask) {
|
||||||
super(res, bitmap);
|
super(res, bitmap);
|
||||||
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
loaderTaskReference = new WeakReference<>(loaderTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoaderTask getLoaderTask() {
|
public LoaderTask getLoaderTask() {
|
||||||
@@ -280,34 +296,38 @@ public class CachedAppAssetLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void populateImageView(NvApp app, ImageView view) {
|
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
||||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
// If there's already a task in progress for this view,
|
// If there's already a task in progress for this view,
|
||||||
// cancel it. If the task is already loading the same image,
|
// cancel it. If the task is already loading the same image,
|
||||||
// we return and let that load finish.
|
// we return and let that load finish.
|
||||||
if (!cancelPendingLoad(tuple, view)) {
|
if (!cancelPendingLoad(tuple, imgView)) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the progress bar always on initial load
|
||||||
|
prgView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
// First, try the memory cache in the current context
|
// First, try the memory cache in the current context
|
||||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
// Show the bitmap immediately
|
// Show the bitmap immediately
|
||||||
view.setAlpha(1.0f);
|
imgView.setVisibility(View.VISIBLE);
|
||||||
view.setImageBitmap(bmp);
|
imgView.setImageBitmap(bmp);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's not in memory, create an async task to load it. This task will be attached
|
// If it's not in memory, create an async task to load it. This task will be attached
|
||||||
// via AsyncDrawable to this view.
|
// via AsyncDrawable to this view.
|
||||||
final LoaderTask task = new LoaderTask(view, true);
|
final LoaderTask task = new LoaderTask(imgView, prgView, true);
|
||||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
|
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||||
view.setAlpha(0.0f);
|
imgView.setVisibility(View.INVISIBLE);
|
||||||
view.setImageDrawable(asyncDrawable);
|
imgView.setImageDrawable(asyncDrawable);
|
||||||
|
|
||||||
// Run the task on our foreground executor
|
// Run the task on our foreground executor
|
||||||
task.executeOnExecutor(foregroundExecutor, tuple);
|
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LoaderTuple {
|
public class LoaderTuple {
|
||||||
|
|||||||
@@ -1,58 +1,133 @@
|
|||||||
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;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class DiskAssetLoader {
|
public class DiskAssetLoader {
|
||||||
// 5 MB
|
// 5 MB
|
||||||
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
// Standard box art is 300x400
|
||||||
|
private static final int STANDARD_ASSET_WIDTH = 300;
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
||||||
InputStream in = null;
|
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||||
Bitmap bmp = null;
|
// Raw height and width of image
|
||||||
try {
|
final int height = options.outHeight;
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
final int width = options.outWidth;
|
||||||
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
int inSampleSize = 1;
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
if (height > reqHeight || width > reqWidth) {
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = sampleSize;
|
final int halfHeight = height / 2;
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
final int halfWidth = width / 2;
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
|
||||||
} catch (IOException ignored) {
|
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
||||||
} finally {
|
// height and width larger than the requested height and width.
|
||||||
if (in != null) {
|
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||||
try {
|
inSampleSize *= 2;
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bmp != null) {
|
return inSampleSize;
|
||||||
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
|
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||||
|
|
||||||
|
// Don't bother with anything if it doesn't exist
|
||||||
|
if (!file.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the cached asset doesn't exceed the maximum size
|
||||||
|
if (file.length() > MAX_ASSET_SIZE) {
|
||||||
|
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||||
|
file.delete();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bmp;
|
||||||
|
|
||||||
|
// For OSes prior to P, we have to use the ugly BitmapFactory API
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
// Lookup bounds of the downloaded image
|
||||||
|
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
||||||
|
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 {
|
||||||
|
// On P, we can get a bitmap back in one step with ImageDecoder
|
||||||
|
try {
|
||||||
|
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||||
|
@Override
|
||||||
|
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
||||||
|
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
||||||
|
if (isLowRamDevice) {
|
||||||
|
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bmp;
|
return bmp;
|
||||||
@@ -62,7 +137,7 @@ public class DiskAssetLoader {
|
|||||||
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) {
|
||||||
@@ -76,7 +151,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) {
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import android.content.Context;
|
|||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.InetAddress;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
public class NetworkAssetLoader {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@@ -21,10 +20,10 @@ public class NetworkAssetLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
|
||||||
|
|
||||||
InputStream in = null;
|
InputStream in = null;
|
||||||
try {
|
try {
|
||||||
|
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) {}
|
||||||
|
|
||||||
@@ -37,13 +36,4 @@ public class NetworkAssetLoader {
|
|||||||
|
|
||||||
return in;
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
|
||||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
|
||||||
return computer.localIp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return computer.remoteIp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.net.InterfaceAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Locale;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -17,10 +21,10 @@ import android.content.ComponentName;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.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;
|
||||||
@@ -29,7 +33,7 @@ import android.widget.Toast;
|
|||||||
public class AddComputerManually extends Activity {
|
public class AddComputerManually extends Activity {
|
||||||
private TextView hostText;
|
private TextView hostText;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
||||||
private Thread addThread;
|
private Thread addThread;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||||
@@ -43,42 +47,90 @@ public class AddComputerManually extends Activity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private boolean isWrongSubnetSiteLocalAddress(String address) {
|
||||||
|
try {
|
||||||
|
InetAddress targetAddress = InetAddress.getByName(address);
|
||||||
|
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a site-local address. Look for a matching local interface.
|
||||||
|
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
||||||
|
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
||||||
|
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
|
||||||
|
// Skip non-site-local or non-IPv4 addresses
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] targetAddrBytes = targetAddress.getAddress();
|
||||||
|
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
|
||||||
|
|
||||||
|
// Compare prefix to ensure it's the same
|
||||||
|
boolean addressMatches = true;
|
||||||
|
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
|
||||||
|
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
|
||||||
|
addressMatches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addressMatches) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't find a matching interface
|
||||||
|
return true;
|
||||||
|
} catch (SocketException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
boolean wrongSiteLocal = false;
|
||||||
boolean finish = false;
|
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);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InetAddress addr = InetAddress.getByName(host);
|
success = managerBinder.addComputerBlocking(host, true);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
if (!managerBinder.addComputerBlocking(addr)){
|
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||||
msg = getResources().getString(R.string.addpc_fail);
|
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||||
}
|
e.printStackTrace();
|
||||||
else {
|
success = false;
|
||||||
msg = getResources().getString(R.string.addpc_success);
|
}
|
||||||
finish = true;
|
if (!success){
|
||||||
}
|
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
msg = getResources().getString(R.string.addpc_unknown_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
final boolean toastFinish = finish;
|
if (wrongSiteLocal) {
|
||||||
final String toastMsg = msg;
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
}
|
||||||
@Override
|
else if (!success) {
|
||||||
public void run() {
|
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
||||||
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
}
|
||||||
|
else {
|
||||||
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
if (toastFinish && !isFinishing()) {
|
if (!isFinishing()) {
|
||||||
// Close the activity
|
// Close the activity
|
||||||
AddComputerManually.this.finish();
|
AddComputerManually.this.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startAddThread() {
|
private void startAddThread() {
|
||||||
@@ -136,18 +188,13 @@ public class AddComputerManually extends Activity {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
UiHelper.setLocale(this);
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
|
||||||
config.locale = new Locale(locale);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_add_computer_manually);
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
this.hostText = findViewById(R.id.hostTextView);
|
||||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -156,12 +203,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
|
||||||
@@ -174,8 +216,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.preference.DialogPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
|
||||||
|
|
||||||
|
public class ConfirmDeleteOscPreference extends DialogPreference {
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
public ConfirmDeleteOscPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||||
|
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
|
||||||
|
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
public class GlPreferences {
|
||||||
|
private static final String PREF_NAME = "GlPreferences";
|
||||||
|
|
||||||
|
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
|
||||||
|
private static final String GL_RENDERER_PREF_STRING = "Renderer";
|
||||||
|
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
public String glRenderer;
|
||||||
|
public String savedFingerprint;
|
||||||
|
|
||||||
|
private GlPreferences(SharedPreferences prefs) {
|
||||||
|
this.prefs = prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GlPreferences readPreferences(Context context) {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
|
||||||
|
GlPreferences glPrefs = new GlPreferences(prefs);
|
||||||
|
|
||||||
|
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
|
||||||
|
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
|
||||||
|
|
||||||
|
return glPrefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean writePreferences() {
|
||||||
|
return prefs.edit()
|
||||||
|
.putString(GL_RENDERER_PREF_STRING, glRenderer)
|
||||||
|
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,13 @@ 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 BITRATE_PREF_STRING = "seekbar_bitrate";
|
|
||||||
|
|
||||||
|
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
||||||
|
static final String FPS_PREF_STRING = "list_fps";
|
||||||
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
||||||
|
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||||
@@ -21,16 +26,20 @@ public class PreferenceConfiguration {
|
|||||||
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
||||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||||
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 ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
||||||
|
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
||||||
|
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
||||||
|
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||||
|
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||||
|
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
||||||
|
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
||||||
|
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
||||||
|
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
||||||
|
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
static final String DEFAULT_RESOLUTION = "720p";
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
static final String DEFAULT_FPS = "60";
|
||||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
|
||||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
|
||||||
private static final int BITRATE_DEFAULT_4K_30 = 40;
|
|
||||||
private static final int BITRATE_DEFAULT_4K_60 = 80;
|
|
||||||
|
|
||||||
private static final String DEFAULT_RES_FPS = "720p60";
|
|
||||||
private static final 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;
|
||||||
@@ -42,6 +51,17 @@ public class PreferenceConfiguration {
|
|||||||
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
||||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||||
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 ONLY_L3_R3_DEFAULT = false;
|
||||||
|
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||||
|
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||||
|
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||||
|
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||||
|
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||||
|
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||||
|
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||||
|
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||||
|
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||||
|
|
||||||
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;
|
||||||
@@ -54,29 +74,104 @@ public class PreferenceConfiguration {
|
|||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||||
|
public boolean onscreenController;
|
||||||
|
public boolean onlyL3R3;
|
||||||
|
public boolean disableFrameDrop;
|
||||||
|
public boolean enableHdr;
|
||||||
|
public boolean enablePip;
|
||||||
|
public boolean bindAllUsb;
|
||||||
|
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("720p30")) {
|
if (resString.equalsIgnoreCase("360p")) {
|
||||||
return BITRATE_DEFAULT_720_30;
|
return 360;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("720p60")) {
|
else if (resString.equalsIgnoreCase("480p")) {
|
||||||
return BITRATE_DEFAULT_720_60;
|
return 480;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("1080p30")) {
|
else if (resString.equalsIgnoreCase("720p")) {
|
||||||
return BITRATE_DEFAULT_1080_30;
|
return 720;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("1080p60")) {
|
else if (resString.equalsIgnoreCase("1080p")) {
|
||||||
return BITRATE_DEFAULT_1080_60;
|
return 1080;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("4K30")) {
|
else if (resString.equalsIgnoreCase("1440p")) {
|
||||||
return BITRATE_DEFAULT_4K_30;
|
return 1440;
|
||||||
}
|
}
|
||||||
else if (resFpsString.equals("4K60")) {
|
else if (resString.equalsIgnoreCase("4K")) {
|
||||||
return BITRATE_DEFAULT_4K_60;
|
return 2160;
|
||||||
}
|
}
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,12 +192,14 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use small mode on anything smaller than a 7" tablet
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
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) {
|
||||||
@@ -124,47 +221,93 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void resetStreamingSettings(Context context) {
|
||||||
|
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
||||||
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
prefs.edit()
|
||||||
|
.remove(BITRATE_PREF_STRING)
|
||||||
|
.remove(BITRATE_PREF_OLD_STRING)
|
||||||
|
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||||
|
.remove(RESOLUTION_PREF_STRING)
|
||||||
|
.remove(FPS_PREF_STRING)
|
||||||
|
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||||
|
.remove(ENABLE_HDR_PREF_STRING)
|
||||||
|
.remove(UNLOCK_FPS_STRING)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
public static PreferenceConfiguration readPreferences(Context context) {
|
public static PreferenceConfiguration readPreferences(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
// Migrate legacy preferences to the new locations
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||||
if (str.equals("720p30")) {
|
if (str != null) {
|
||||||
config.width = 1280;
|
if (str.equals("360p30")) {
|
||||||
config.height = 720;
|
config.width = 640;
|
||||||
config.fps = 30;
|
config.height = 360;
|
||||||
}
|
config.fps = 30;
|
||||||
else if (str.equals("720p60")) {
|
}
|
||||||
config.width = 1280;
|
else if (str.equals("360p60")) {
|
||||||
config.height = 720;
|
config.width = 640;
|
||||||
config.fps = 60;
|
config.height = 360;
|
||||||
}
|
config.fps = 60;
|
||||||
else if (str.equals("1080p30")) {
|
}
|
||||||
config.width = 1920;
|
else if (str.equals("720p30")) {
|
||||||
config.height = 1080;
|
config.width = 1280;
|
||||||
config.fps = 30;
|
config.height = 720;
|
||||||
}
|
config.fps = 30;
|
||||||
else if (str.equals("1080p60")) {
|
}
|
||||||
config.width = 1920;
|
else if (str.equals("720p60")) {
|
||||||
config.height = 1080;
|
config.width = 1280;
|
||||||
config.fps = 60;
|
config.height = 720;
|
||||||
}
|
config.fps = 60;
|
||||||
else if (str.equals("4K30")) {
|
}
|
||||||
config.width = 3840;
|
else if (str.equals("1080p30")) {
|
||||||
config.height = 2160;
|
config.width = 1920;
|
||||||
config.fps = 30;
|
config.height = 1080;
|
||||||
}
|
config.fps = 30;
|
||||||
else if (str.equals("4K60")) {
|
}
|
||||||
config.width = 3840;
|
else if (str.equals("1080p60")) {
|
||||||
config.height = 2160;
|
config.width = 1920;
|
||||||
config.fps = 60;
|
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 {
|
else {
|
||||||
// Should never get here
|
// Use the new preference location
|
||||||
config.width = 1280;
|
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
config.height = 720;
|
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||||
config.fps = 60;
|
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);
|
||||||
|
if (config.bitrate == 0) {
|
||||||
|
config.bitrate = getDefaultBitrate(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
config.videoFormat = getVideoFormatValue(context);
|
||||||
@@ -183,6 +326,17 @@ public class PreferenceConfiguration {
|
|||||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||||
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.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||||
|
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
||||||
|
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||||
|
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||||
|
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||||
|
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||||
|
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||||
|
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||||
|
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||||
|
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import android.widget.TextView;
|
|||||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||||
public class SeekBarPreference extends DialogPreference
|
public class SeekBarPreference extends DialogPreference
|
||||||
{
|
{
|
||||||
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
private static final String ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||||
|
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
|
||||||
|
|
||||||
private SeekBar seekBar;
|
private SeekBar seekBar;
|
||||||
private TextView valueText;
|
private TextView valueText;
|
||||||
@@ -27,6 +28,7 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
private final int defaultValue;
|
private final int defaultValue;
|
||||||
private final int maxValue;
|
private final int maxValue;
|
||||||
private final int minValue;
|
private final int minValue;
|
||||||
|
private final int stepSize;
|
||||||
private int currentValue;
|
private int currentValue;
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
@@ -34,27 +36,28 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
// Read the message from XML
|
// Read the message from XML
|
||||||
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
|
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
|
||||||
if (dialogMessageId == 0) {
|
if (dialogMessageId == 0) {
|
||||||
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
|
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dialogMessage = context.getString(dialogMessageId);
|
dialogMessage = context.getString(dialogMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the suffix for the number displayed in the dialog
|
// Get the suffix for the number displayed in the dialog
|
||||||
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
|
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
|
||||||
if (suffixId == 0) {
|
if (suffixId == 0) {
|
||||||
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
|
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
suffix = context.getString(suffixId);
|
suffix = context.getString(suffixId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default, min, and max seekbar values
|
// Get default, min, and max seekbar values
|
||||||
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
||||||
minValue = 1;
|
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
||||||
|
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -89,6 +92,12 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
|
||||||
|
if (roundedValue != value) {
|
||||||
|
seekBar.setProgress(roundedValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t = String.valueOf(value);
|
||||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,38 +2,45 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.media.MediaCodecInfo;
|
||||||
|
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.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.util.Range;
|
||||||
|
import android.view.Display;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.video.MediaCodecHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
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);
|
||||||
|
|
||||||
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
||||||
|
|
||||||
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
UiHelper.setLocale(this);
|
||||||
Configuration config = new Configuration(getResources().getConfiguration());
|
|
||||||
config.locale = new Locale(previousPrefs.language);
|
|
||||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
setContentView(R.layout.activity_stream_settings);
|
||||||
getFragmentManager().beginTransaction().replace(
|
reloadSettings();
|
||||||
R.id.stream_settings, new SettingsFragment()
|
|
||||||
).commit();
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
}
|
}
|
||||||
@@ -55,26 +62,320 @@ public class StreamSettings extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
|
|
||||||
|
private void setValue(String preferenceKey, String value) {
|
||||||
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
|
|
||||||
|
pref.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||||
|
int matchingCount = 0;
|
||||||
|
|
||||||
|
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||||
|
|
||||||
|
// Count the number of matching entries we'll be removing
|
||||||
|
for (CharSequence seq : pref.getEntryValues()) {
|
||||||
|
if (seq.toString().equalsIgnoreCase(value)) {
|
||||||
|
matchingCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new arrays
|
||||||
|
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
|
||||||
|
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||||
|
int outIndex = 0;
|
||||||
|
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||||
|
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||||
|
// Skip matching values
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries[outIndex] = pref.getEntries()[i];
|
||||||
|
entryValues[outIndex] = pref.getEntryValues()[i];
|
||||||
|
outIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||||
|
onMatched.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the preference with the new list
|
||||||
|
pref.setEntries(entries);
|
||||||
|
pref.setEntryValues(entryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||||
|
if (res == null) {
|
||||||
|
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||||
|
}
|
||||||
|
if (fps == null) {
|
||||||
|
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit()
|
||||||
|
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||||
|
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
PreferenceScreen screen = getPreferenceScreen();
|
||||||
|
|
||||||
|
// hide on-screen controls category on non touch screen devices
|
||||||
|
if (!getActivity().getPackageManager().
|
||||||
|
hasSystemFeature("android.hardware.touchscreen")) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||||
|
screen.removePreference(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove PiP mode on devices pre-Oreo
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_basic_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxSupportedFps = 0;
|
||||||
|
|
||||||
|
// Hide non-supported resolution/FPS combinations
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||||
|
|
||||||
|
int maxSupportedResW = 0;
|
||||||
|
|
||||||
|
// Always allow resolutions that are smaller or equal to the active
|
||||||
|
// display resolution because decoders can report total non-sense to us.
|
||||||
|
// For example, a p201 device reports:
|
||||||
|
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
|
||||||
|
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
||||||
|
// AVC supported width range: 64 - 384
|
||||||
|
// HEVC supported width range: 64 - 544
|
||||||
|
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||||
|
// Some devices report their dimensions in the portrait orientation
|
||||||
|
// where height > width. Normalize these to the conventional width > height
|
||||||
|
// arrangement before we process them.
|
||||||
|
|
||||||
|
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||||
|
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||||
|
|
||||||
|
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||||
|
maxSupportedResW = 3840;
|
||||||
|
}
|
||||||
|
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
||||||
|
maxSupportedResW = 2560;
|
||||||
|
}
|
||||||
|
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
||||||
|
maxSupportedResW = 1920;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate.getRefreshRate() > maxSupportedFps) {
|
||||||
|
maxSupportedFps = (int)candidate.getRefreshRate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must be called to do runtime initialization before calling functions that evaluate
|
||||||
|
// decoder lists.
|
||||||
|
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
||||||
|
|
||||||
|
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
|
||||||
|
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||||
|
|
||||||
|
if (avcDecoder != null) {
|
||||||
|
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
||||||
|
|
||||||
|
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
|
||||||
|
|
||||||
|
// If 720p is not reported as supported, ignore all results from this API
|
||||||
|
if (avcWidthRange.contains(1280)) {
|
||||||
|
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
||||||
|
maxSupportedResW = 3840;
|
||||||
|
}
|
||||||
|
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
||||||
|
maxSupportedResW = 1920;
|
||||||
|
}
|
||||||
|
else if (maxSupportedResW < 1280) {
|
||||||
|
maxSupportedResW = 1280;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hevcDecoder != null) {
|
||||||
|
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
||||||
|
|
||||||
|
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
|
||||||
|
|
||||||
|
// If 720p is not reported as supported, ignore all results from this API
|
||||||
|
if (hevcWidthRange.contains(1280)) {
|
||||||
|
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
||||||
|
maxSupportedResW = 3840;
|
||||||
|
}
|
||||||
|
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
||||||
|
maxSupportedResW = 1920;
|
||||||
|
}
|
||||||
|
else if (maxSupportedResW < 1280) {
|
||||||
|
maxSupportedResW = 1280;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
||||||
|
|
||||||
|
if (maxSupportedResW != 0) {
|
||||||
|
if (maxSupportedResW < 3840) {
|
||||||
|
// 4K is unsupported
|
||||||
|
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) {
|
||||||
|
// 1080p is unsupported
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
LimeLog.info("Excluding HDR toggle based on OS");
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_enable_hdr"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||||
|
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
||||||
|
|
||||||
|
// We must now ensure our display is compatible with HDR10
|
||||||
|
boolean foundHdr10 = false;
|
||||||
|
if (hdrCaps != null) {
|
||||||
|
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||||
|
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||||
|
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||||
|
foundHdr10 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundHdr10) {
|
||||||
|
LimeLog.info("Excluding HDR toggle based on display capabilities");
|
||||||
|
PreferenceCategory category =
|
||||||
|
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||||
|
category.removePreference(findPreference("checkbox_enable_hdr"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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,6 +3,6 @@ package com.limelight.ui;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
public interface AdapterFragmentCallbacks {
|
public interface AdapterFragmentCallbacks {
|
||||||
public int getAdapterFragmentLayoutId();
|
int getAdapterFragmentLayoutId();
|
||||||
public void receiveAbsListView(AbsListView gridView);
|
void receiveAbsListView(AbsListView gridView);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.limelight.ui;
|
package com.limelight.ui;
|
||||||
|
|
||||||
public interface GameGestures {
|
public interface GameGestures {
|
||||||
public void showKeyboard();
|
void showKeyboard();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.limelight.ui;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.SurfaceView;
|
||||||
|
|
||||||
|
public class StreamView extends SurfaceView {
|
||||||
|
private double desiredAspectRatio;
|
||||||
|
private InputCallbacks inputCallbacks;
|
||||||
|
|
||||||
|
public void setDesiredAspectRatio(double aspectRatio) {
|
||||||
|
this.desiredAspectRatio = aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputCallbacks(InputCallbacks callbacks) {
|
||||||
|
this.inputCallbacks = callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(21)
|
||||||
|
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||||
|
if (desiredAspectRatio == 0) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||||
|
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
|
||||||
|
int measuredHeight, measuredWidth;
|
||||||
|
if (widthSize > heightSize * desiredAspectRatio) {
|
||||||
|
measuredHeight = heightSize;
|
||||||
|
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||||
|
} else {
|
||||||
|
measuredWidth = widthSize;
|
||||||
|
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import java.io.OutputStream;
|
|||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
|
||||||
public class CacheHelper {
|
public class CacheHelper {
|
||||||
private static File openPath(boolean createPath, File root, String... path) {
|
public static File openPath(boolean createPath, File root, String... path) {
|
||||||
File f = root;
|
File f = root;
|
||||||
for (int i = 0; i < path.length; i++) {
|
for (int i = 0; i < path.length; i++) {
|
||||||
String component = path[i];
|
String component = path[i];
|
||||||
|
|||||||
@@ -5,23 +5,26 @@ import java.util.ArrayList;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
public class Dialog implements Runnable {
|
public class Dialog implements Runnable {
|
||||||
private final String title;
|
private final String title;
|
||||||
private final String message;
|
private final String message;
|
||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
private final boolean endAfterDismiss;
|
private final Runnable runOnDismiss;
|
||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.endAfterDismiss = endAfterDismiss;
|
this.runOnDismiss = runOnDismiss;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
@@ -37,9 +40,21 @@ public class Dialog implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (endAfterDismiss) {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
||||||
|
{
|
||||||
|
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,18 +70,39 @@ public class Dialog implements Runnable {
|
|||||||
alert.setCancelable(false);
|
alert.setCancelable(false);
|
||||||
alert.setCanceledOnTouchOutside(false);
|
alert.setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(Dialog.this);
|
rundownDialogs.remove(Dialog.this);
|
||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endAfterDismiss) {
|
runOnDismiss.run();
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
rundownDialogs.remove(Dialog.this);
|
||||||
|
alert.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnDismiss.run();
|
||||||
|
|
||||||
|
HelpLauncher.launchTroubleshooting(activity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
alert.setOnShowListener(new DialogInterface.OnShowListener(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShow(DialogInterface dialog) {
|
||||||
|
// Set focus to the OK button by default
|
||||||
|
Button button = alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||||
|
button.setFocusable(true);
|
||||||
|
button.setFocusableInTouchMode(true);
|
||||||
|
button.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.add(this);
|
rundownDialogs.add(this);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.limelight.HelpActivity;
|
||||||
|
|
||||||
|
public class HelpLauncher {
|
||||||
|
|
||||||
|
private static boolean isKnownBrowser(Context context, Intent i) {
|
||||||
|
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
||||||
|
if (resolvedActivity == null) {
|
||||||
|
// No browser
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = resolvedActivity.activityInfo.name;
|
||||||
|
if (name == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name.toLowerCase();
|
||||||
|
return name.contains("chrome") || name.contains("firefox");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void launchUrl(Context context, String url) {
|
||||||
|
// Try to launch the default browser
|
||||||
|
try {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse(url));
|
||||||
|
|
||||||
|
// Several Android TV devices will lie and say they do have a browser
|
||||||
|
// even though the OS just shows an error dialog if we try to use it. We need to
|
||||||
|
// be a bit more clever on these devices and detect if the browser is a legitimate
|
||||||
|
// browser or just a fake error message activity.
|
||||||
|
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
||||||
|
isKnownBrowser(context, i)) {
|
||||||
|
context.startActivity(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// This is only supposed to throw ActivityNotFoundException but
|
||||||
|
// it can (at least) also throw SecurityException if a user's default
|
||||||
|
// browser is not exported. We'll catch everything to workaround this.
|
||||||
|
|
||||||
|
// Fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
// This platform has no browser (possibly a leanback device)
|
||||||
|
// We'll launch our WebView activity
|
||||||
|
Intent i = new Intent(context, HelpActivity.class);
|
||||||
|
i.setData(Uri.parse(url));
|
||||||
|
context.startActivity(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launchSetupGuide(Context context) {
|
||||||
|
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void launchTroubleshooting(Context context) {
|
||||||
|
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,32 +13,50 @@ 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.net.InetAddress;
|
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 InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
return computer.activeAddress;
|
||||||
computer.localIp : computer.remoteIp;
|
}
|
||||||
|
|
||||||
|
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
Intent intent = new Intent(parent, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
|
||||||
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
|
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
|
||||||
|
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||||
|
try {
|
||||||
|
if (computer.serverCert != null) {
|
||||||
|
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
||||||
|
}
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
Intent intent = new Intent(parent, Game.class);
|
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||||
intent.putExtra(Game.EXTRA_HOST,
|
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
return;
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
}
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
|
||||||
parent.startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
public static void doQuit(final Activity parent,
|
||||||
final InetAddress address,
|
final ComputerDetails computer,
|
||||||
final NvApp app,
|
final NvApp app,
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
final Runnable onComplete) {
|
final Runnable onComplete) {
|
||||||
@@ -49,8 +67,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 {
|
||||||
@@ -69,8 +87,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();
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ShortcutInfo;
|
||||||
|
import android.content.pm.ShortcutManager;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.Icon;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.ShortcutTrampoline;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ShortcutHelper {
|
||||||
|
|
||||||
|
private final ShortcutManager sm;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public ShortcutHelper(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
sm = context.getSystemService(ShortcutManager.class);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sm = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private void reapShortcutsForDynamicAdd() {
|
||||||
|
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
||||||
|
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
||||||
|
for (ShortcutInfo scut : dynamicShortcuts) {
|
||||||
|
if (maxRankShortcut.getRank() < scut.getRank()) {
|
||||||
|
maxRankShortcut = scut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sm.removeDynamicShortcuts(Collections.singletonList(maxRankShortcut.getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private List<ShortcutInfo> getAllShortcuts() {
|
||||||
|
LinkedList<ShortcutInfo> list = new LinkedList<>();
|
||||||
|
list.addAll(sm.getDynamicShortcuts());
|
||||||
|
list.addAll(sm.getPinnedShortcuts());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private ShortcutInfo getInfoForId(String id) {
|
||||||
|
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||||
|
|
||||||
|
for (ShortcutInfo info : shortcuts) {
|
||||||
|
if (info.getId().equals(id)) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||||
|
private boolean isExistingDynamicShortcut(String id) {
|
||||||
|
for (ShortcutInfo si : sm.getDynamicShortcuts()) {
|
||||||
|
if (si.getId().equals(id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportShortcutUsed(String id) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
if (getInfoForId(id) != null) {
|
||||||
|
sm.reportShortcutUsed(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
|
||||||
|
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||||
|
.setIntent(i)
|
||||||
|
.setShortLabel(computerName)
|
||||||
|
.setLongLabel(computerName)
|
||||||
|
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||||
|
if (existingSinfo != null) {
|
||||||
|
// Update in place
|
||||||
|
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||||
|
sm.enableShortcuts(Collections.singletonList(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reap shortcuts to make space for this if it's new
|
||||||
|
// NOTE: This CAN'T be an else on the above if, because it's
|
||||||
|
// possible that we have an existing shortcut but it's not a dynamic one.
|
||||||
|
if (!isExistingDynamicShortcut(id)) {
|
||||||
|
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||||
|
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||||
|
// to interact with this PC.
|
||||||
|
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
||||||
|
reapShortcutsForDynamicAdd();
|
||||||
|
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||||
|
createAppViewShortcut(id, details.name, details.uuid, forceAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
|
||||||
|
if (sm.isRequestPinShortcutSupported()) {
|
||||||
|
Icon appIcon;
|
||||||
|
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||||
|
|
||||||
|
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||||
|
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||||
|
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
|
||||||
|
i.setAction(Intent.ACTION_DEFAULT);
|
||||||
|
|
||||||
|
if (iconBits != null) {
|
||||||
|
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
|
||||||
|
} else {
|
||||||
|
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
|
||||||
|
.setIntent(i)
|
||||||
|
.setShortLabel(appName + " (" + computerName + ")")
|
||||||
|
.setIcon(appIcon)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return sm.requestPinShortcut(sInfo, null);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
|
||||||
|
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableShortcut(String id, CharSequence reason) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
if (getInfoForId(id) != null) {
|
||||||
|
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
|||||||
private ProgressDialog progress;
|
private ProgressDialog progress;
|
||||||
private final boolean finish;
|
private final boolean finish;
|
||||||
|
|
||||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
|
||||||
|
|
||||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import android.app.AlertDialog;
|
|||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.os.Build;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
@@ -16,6 +23,28 @@ public class UiHelper {
|
|||||||
private static final int TV_VERTICAL_PADDING_DP = 27;
|
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||||
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||||
|
|
||||||
|
public static void setLocale(Activity activity)
|
||||||
|
{
|
||||||
|
String locale = PreferenceConfiguration.readPreferences(activity).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
|
Configuration config = new Configuration(activity.getResources().getConfiguration());
|
||||||
|
|
||||||
|
// Some locales include both language and country which must be separated
|
||||||
|
// before calling the Locale constructor.
|
||||||
|
if (locale.contains("-"))
|
||||||
|
{
|
||||||
|
config.locale = new Locale(locale.substring(0, locale.indexOf('-')),
|
||||||
|
locale.substring(locale.indexOf('-') + 1));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.getResources().updateConfiguration(config, activity.getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void notifyNewRootView(Activity activity)
|
public static void notifyNewRootView(Activity activity)
|
||||||
{
|
{
|
||||||
View rootView = activity.findViewById(android.R.id.content);
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
@@ -31,6 +60,54 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showDecoderCrashDialog(Activity activity) {
|
||||||
|
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
|
||||||
|
final int crashCount = prefs.getInt("CrashCount", 0);
|
||||||
|
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
||||||
|
|
||||||
|
// Remember the last crash count we notified at, so we don't
|
||||||
|
// display the crash dialog every time the app is started until
|
||||||
|
// they stream again
|
||||||
|
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
||||||
|
if (crashCount % 3 == 0) {
|
||||||
|
// At 3 consecutive crashes, we'll forcefully reset their settings
|
||||||
|
PreferenceConfiguration.resetStreamingSettings(activity);
|
||||||
|
Dialog.displayDialog(activity,
|
||||||
|
activity.getResources().getString(R.string.title_decoding_reset),
|
||||||
|
activity.getResources().getString(R.string.message_decoding_reset),
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Mark notification as acknowledged on dismissal
|
||||||
|
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Dialog.displayDialog(activity,
|
||||||
|
activity.getResources().getString(R.string.title_decoding_error),
|
||||||
|
activity.getResources().getString(R.string.message_decoding_error),
|
||||||
|
new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Mark notification as acknowledged on dismissal
|
||||||
|
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||||
@@ -59,4 +136,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,47 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
public class Vector2d {
|
||||||
|
private float x;
|
||||||
|
private float y;
|
||||||
|
private double magnitude;
|
||||||
|
|
||||||
|
public static final Vector2d ZERO = new Vector2d();
|
||||||
|
|
||||||
|
public Vector2d() {
|
||||||
|
initialize(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize(float x, float y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMagnitude() {
|
||||||
|
return magnitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getNormalized(Vector2d vector) {
|
||||||
|
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scalarMultiply(double factor) {
|
||||||
|
initialize((float)(x * factor), (float)(y * factor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setX(float x) {
|
||||||
|
initialize(x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setY(float y) {
|
||||||
|
initialize(this.x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
# Application.mk for Limelight
|
# Application.mk for Moonlight
|
||||||
|
|
||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
# Support all modern ABIs
|
|
||||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
|
|
||||||
|
|
||||||
# We want an optimized build
|
|
||||||
APP_OPTIM := release
|
|
||||||
|
|||||||
@@ -5,28 +5,32 @@ include $(call all-subdir-makefiles)
|
|||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
# Only build evdev_reader for the rooted APK flavor
|
||||||
LOCAL_MODULE := evdev_reader
|
ifeq (root,$(PRODUCT_FLAVOR))
|
||||||
LOCAL_SRC_FILES := evdev_reader.c
|
include $(CLEAR_VARS)
|
||||||
LOCAL_LDLIBS := -llog
|
LOCAL_MODULE := evdev_reader
|
||||||
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
|
LOCAL_LDLIBS := -llog
|
||||||
|
|
||||||
|
|
||||||
# This next portion of the makefile is mostly copied from build-executable.mk but
|
# This next portion of the makefile is mostly copied from build-executable.mk but
|
||||||
# creates a binary with the libXXX.so form so the APK will install and drop
|
# creates a binary with the libXXX.so form so the APK will install and drop
|
||||||
# the binary correctly.
|
# the binary correctly.
|
||||||
|
|
||||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||||
LOCAL_MAKEFILE := $(local-makefile)
|
LOCAL_MAKEFILE := $(local-makefile)
|
||||||
|
|
||||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||||
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
||||||
$(call check-LOCAL_MODULE_FILENAME)
|
$(call check-LOCAL_MODULE_FILENAME)
|
||||||
|
|
||||||
# we are building target objects
|
# we are building target objects
|
||||||
my := TARGET_
|
my := TARGET_
|
||||||
|
|
||||||
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
||||||
$(call handle-module-built)
|
$(call handle-module-built)
|
||||||
|
|
||||||
|
LOCAL_MODULE_CLASS := EXECUTABLE
|
||||||
|
include $(BUILD_SYSTEM)/build-module.mk
|
||||||
|
endif
|
||||||
|
|
||||||
LOCAL_MODULE_CLASS := EXECUTABLE
|
|
||||||
include $(BUILD_SYSTEM)/build-module.mk
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <linux/input.h>
|
#include <linux/input.h>
|
||||||
@@ -11,9 +12,15 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define EVDEV_MAX_EVENT_SIZE 24
|
||||||
|
|
||||||
#define REL_X 0x00
|
#define REL_X 0x00
|
||||||
#define REL_Y 0x01
|
#define REL_Y 0x01
|
||||||
#define KEY_Q 16
|
#define KEY_Q 16
|
||||||
@@ -30,9 +37,11 @@ struct DeviceEntry {
|
|||||||
static struct DeviceEntry *DeviceListHead;
|
static struct DeviceEntry *DeviceListHead;
|
||||||
static int grabbing = 1;
|
static int grabbing = 1;
|
||||||
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static pthread_mutex_t SocketSendLock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static int sock;
|
||||||
|
|
||||||
// This is a small executable that runs in a root shell. It reads input
|
// This is a small executable that runs in a root shell. It reads input
|
||||||
// devices and writes the evdev output packets to stdout. This allows
|
// devices and writes the evdev output packets to a socket. This allows
|
||||||
// Moonlight to read input devices without having to muck with changing
|
// Moonlight to read input devices without having to muck with changing
|
||||||
// device permissions or modifying SELinux policy (which is prevented in
|
// device permissions or modifying SELinux policy (which is prevented in
|
||||||
// Marshmallow anyway).
|
// Marshmallow anyway).
|
||||||
@@ -56,20 +65,23 @@ static int hasKey(int fd, short key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void outputEvdevData(char *data, int dataSize) {
|
static void outputEvdevData(char *data, int dataSize) {
|
||||||
// We need to lock stdout before writing to prevent
|
char packetBuffer[EVDEV_MAX_EVENT_SIZE + sizeof(dataSize)];
|
||||||
// interleaving of data between threads.
|
|
||||||
flockfile(stdout);
|
// Copy the full packet into our buffer
|
||||||
fwrite(&dataSize, sizeof(dataSize), 1, stdout);
|
memcpy(packetBuffer, &dataSize, sizeof(dataSize));
|
||||||
fwrite(data, dataSize, 1, stdout);
|
memcpy(&packetBuffer[sizeof(dataSize)], data, dataSize);
|
||||||
fflush(stdout);
|
|
||||||
funlockfile(stdout);
|
// Lock to prevent other threads from sending at the same time
|
||||||
|
pthread_mutex_lock(&SocketSendLock);
|
||||||
|
send(sock, packetBuffer, dataSize + sizeof(dataSize), 0);
|
||||||
|
pthread_mutex_unlock(&SocketSendLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void* pollThreadFunc(void* context) {
|
void* pollThreadFunc(void* context) {
|
||||||
struct DeviceEntry *device = context;
|
struct DeviceEntry *device = context;
|
||||||
struct pollfd pollinfo;
|
struct pollfd pollinfo;
|
||||||
int pollres, ret;
|
int pollres, ret;
|
||||||
char data[64];
|
char data[EVDEV_MAX_EVENT_SIZE];
|
||||||
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
|
||||||
|
|
||||||
@@ -94,7 +106,7 @@ void* pollThreadFunc(void* context) {
|
|||||||
|
|
||||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
// We'll have data available now
|
// We'll have data available now
|
||||||
ret = read(device->fd, data, sizeof(struct input_event));
|
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
"read() failed: %d", errno);
|
"read() failed: %d", errno);
|
||||||
@@ -132,6 +144,9 @@ cleanup:
|
|||||||
{
|
{
|
||||||
struct DeviceEntry *lastEntry;
|
struct DeviceEntry *lastEntry;
|
||||||
|
|
||||||
|
// Lock the device list
|
||||||
|
pthread_mutex_lock(&DeviceListLock);
|
||||||
|
|
||||||
if (DeviceListHead == device) {
|
if (DeviceListHead == device) {
|
||||||
DeviceListHead = device->next;
|
DeviceListHead = device->next;
|
||||||
}
|
}
|
||||||
@@ -146,6 +161,9 @@ cleanup:
|
|||||||
lastEntry = lastEntry->next;
|
lastEntry = lastEntry->next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlock device list
|
||||||
|
pthread_mutex_unlock(&DeviceListLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free the context
|
// Free the context
|
||||||
@@ -254,6 +272,11 @@ static int enumerateDevices(void) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strstr(dirEnt->d_name, "event") == NULL) {
|
||||||
|
// Skip non-event devices
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
startPollForDevice(dirEnt->d_name);
|
startPollForDevice(dirEnt->d_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +284,39 @@ static int enumerateDevices(void) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int connectSocket(int port) {
|
||||||
|
struct sockaddr_in saddr;
|
||||||
|
int ret;
|
||||||
|
int val;
|
||||||
|
|
||||||
|
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
|
if (sock < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "socket() failed: %d", errno);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&saddr, 0, sizeof(saddr));
|
||||||
|
saddr.sin_family = AF_INET;
|
||||||
|
saddr.sin_port = htons(port);
|
||||||
|
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||||
|
ret = connect(sock, (struct sockaddr*)&saddr, sizeof(saddr));
|
||||||
|
if (ret < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "connect() failed: %d", errno);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
val = 1;
|
||||||
|
ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
|
||||||
|
if (ret < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "setsockopt() failed: %d", errno);
|
||||||
|
// We can continue anyways
|
||||||
|
}
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Connection established to port %d", port);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
#define UNGRAB_REQ 1
|
#define UNGRAB_REQ 1
|
||||||
#define REGRAB_REQ 2
|
#define REGRAB_REQ 2
|
||||||
|
|
||||||
@@ -268,6 +324,18 @@ int main(int argc, char* argv[]) {
|
|||||||
int ret;
|
int ret;
|
||||||
int pollres;
|
int pollres;
|
||||||
struct pollfd pollinfo;
|
struct pollfd pollinfo;
|
||||||
|
int port;
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
|
||||||
|
|
||||||
|
port = atoi(argv[1]);
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
|
||||||
|
|
||||||
|
// Connect to the app's socket
|
||||||
|
ret = connectSocket(port);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
// Perform initial enumeration
|
// Perform initial enumeration
|
||||||
ret = enumerateDevices();
|
ret = enumerateDevices();
|
||||||
@@ -282,7 +350,7 @@ int main(int argc, char* argv[]) {
|
|||||||
do {
|
do {
|
||||||
// Every second we poll again for new devices if
|
// Every second we poll again for new devices if
|
||||||
// we haven't received any new events
|
// we haven't received any new events
|
||||||
pollinfo.fd = STDIN_FILENO;
|
pollinfo.fd = sock;
|
||||||
pollinfo.events = POLLIN;
|
pollinfo.events = POLLIN;
|
||||||
pollinfo.revents = 0;
|
pollinfo.revents = 0;
|
||||||
pollres = poll(&pollinfo, 1, 1000);
|
pollres = poll(&pollinfo, 1, 1000);
|
||||||
@@ -293,33 +361,52 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
while (pollres == 0);
|
while (pollres == 0);
|
||||||
|
|
||||||
ret = fread(&requestId, sizeof(requestId), 1, stdin);
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
if (ret < sizeof(requestId)) {
|
// We'll have data available now
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on input");
|
ret = recv(sock, &requestId, sizeof(requestId), 0);
|
||||||
return errno;
|
if (ret < sizeof(requestId)) {
|
||||||
}
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
|
||||||
|
return errno;
|
||||||
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
|
||||||
return requestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
struct DeviceEntry *currentEntry;
|
|
||||||
|
|
||||||
pthread_mutex_lock(&DeviceListLock);
|
|
||||||
|
|
||||||
// Update state for future devices
|
|
||||||
grabbing = (requestId == REGRAB_REQ);
|
|
||||||
|
|
||||||
// Carry out the requested action on each device
|
|
||||||
currentEntry = DeviceListHead;
|
|
||||||
while (currentEntry != NULL) {
|
|
||||||
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
|
||||||
currentEntry = currentEntry->next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&DeviceListLock);
|
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
||||||
|
return requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
struct DeviceEntry *currentEntry;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&DeviceListLock);
|
||||||
|
|
||||||
|
// Update state for future devices
|
||||||
|
grabbing = (requestId == REGRAB_REQ);
|
||||||
|
|
||||||
|
// Carry out the requested action on each device
|
||||||
|
currentEntry = DeviceListHead;
|
||||||
|
while (currentEntry != NULL) {
|
||||||
|
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
||||||
|
currentEntry = currentEntry->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&DeviceListLock);
|
||||||
|
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "New grab status is: %s",
|
||||||
|
grabbing ? "enabled" : "disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Terminate this thread
|
||||||
|
if (pollres < 0) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Socket recv poll() failed: %d", errno);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
|
"Socket poll unexpected revents: %d", pollinfo.revents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Android.mk for Limelight's Opus decoder
|
|
||||||
MY_LOCAL_PATH := $(call my-dir)
|
|
||||||
|
|
||||||
include $(call all-subdir-makefiles)
|
|
||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE := nv_opus_dec
|
|
||||||
LOCAL_SRC_FILES := nv_opus_dec.c nv_opus_dec_jni.c
|
|
||||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libopus/inc
|
|
||||||
|
|
||||||
# Link to libopus library
|
|
||||||
LOCAL_STATIC_LIBRARIES := libopus
|
|
||||||
|
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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)/inc
|
|
||||||
include $(PREBUILT_STATIC_LIBRARY)
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,100 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,978 +0,0 @@
|
|||||||
/* 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 1 byte, 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(4);
|
|
||||||
|
|
||||||
/** 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 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 */
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
/* 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 */
|
|
||||||
@@ -1,726 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
# ifdef OPUS_BUILD
|
|
||||||
# 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 */
|
|
||||||
|
|
||||||
/* 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 */
|
|
||||||
|
|
||||||
/**@}*/
|
|
||||||
|
|
||||||
|
|
||||||
/** @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.
|
|
||||||
* @warning Only the MDCT mode of Opus can provide hard CBR behavior.
|
|
||||||
* @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 with trigger progressively more loss resistant behavior in the encoder
|
|
||||||
* at the expense of quality at a given bitrate in the lossless case, 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.
|
|
||||||
* @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_VARIABLE_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 2.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_VARIABLE</dt><dd>Optimize the frame size dynamically.</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_VARIABLE_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 2.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_VARIABLE</dt><dd>Optimize the frame size dynamically.</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. (default : 0)
|
|
||||||
* @hideinitializer */
|
|
||||||
#define OPUS_SET_PREDICTION_DISABLED(x) OPUS_SET_PREDICTION_DISABLED_REQUEST, __opus_check_int(x)
|
|
||||||
/** Gets the encoder's configured prediction status.
|
|
||||||
* @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)
|
|
||||||
|
|
||||||
/**@}*/
|
|
||||||
|
|
||||||
/** @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.
|
|
||||||
*
|
|
||||||
* @returns Version string
|
|
||||||
*/
|
|
||||||
OPUS_EXPORT const char *opus_get_version_string(void);
|
|
||||||
/**@}*/
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* OPUS_DEFINES_H */
|
|
||||||
@@ -1,660 +0,0 @@
|
|||||||
/* 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 the
|
|
||||||
* <a href="http://tools.ietf.org/html/draft-terriberry-oggopus">Ogg
|
|
||||||
* encapsulation specification</a> and is based on the self-delimited Opus
|
|
||||||
* framing described in Appendix B of <a href="http://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="http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-800004.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(5);
|
|
||||||
|
|
||||||
/** 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(6);
|
|
||||||
|
|
||||||
/** 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 */
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
/* (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
|
|
||||||
|
|
||||||
/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */
|
|
||||||
#if (defined(__STDC__) && __STDC__ && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H))
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
typedef int16_t opus_int16;
|
|
||||||
typedef uint16_t opus_uint16;
|
|
||||||
typedef int32_t opus_int32;
|
|
||||||
typedef uint32_t opus_uint32;
|
|
||||||
#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
|
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
||||||
#endif /* OPUS_TYPES_H */
|
|
||||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user